Makelore 2.0 initial clean snapshot
This commit is contained in:
85
.opencode/skills/game-assets/SKILL.md
Normal file
85
.opencode/skills/game-assets/SKILL.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: game-assets
|
||||
description: 在 NianCode 中通过 Meowa 的固定版 game-assets 适配器生成和整理像素素材、高清素材、角色、道具、背景、UI 图和可复用游戏资源。素材生成、下载、项目内落盘、授权记录和审核证据必须走这个 Skill。
|
||||
---
|
||||
|
||||
# NianCode · Meowa game-assets
|
||||
|
||||
这是项目内固定的 Meowa `game-assets` Skill 适配版,版本 `2026.06.19.1`。
|
||||
上游参考:`https://github.com/Meowa-AI/meowa-skills/tree/main/skills/game-assets`。
|
||||
|
||||
NianCode 会把 Meowa API Key 留在 Electron Main 的安全存储或启动环境变量中。你只能通过随 Skill 一起提供的 `meowart_api.py` 调用本地 Host API,不能直接访问 Meowa API,也不能读取、打印、复制或写入 API Key。
|
||||
|
||||
## 角色边界
|
||||
|
||||
在 `game-development` 模板中,`game-art` 是唯一直接调用本 Skill 的 Agent。当前 Meowa 适配器只生成 2D 像素/高清光栅素材,不生成 3D 模型;3D 模型应走项目、用户或许可证明确的外部来源,并记录在 `ASSET_PLAN.md`。`game-design` 负责提出视觉缺口和验收语境,`game-development` 只接入审核状态为 `approved` 的项目内素材,`game-test-release` 负责验证真实运行效果并回交问题;它们通过 `GDD.md`、`TECH_STACK.md`、`ASSET_PLAN.md`、`ART_GUIDE.md`、`TASKS.md` 和 `.niancode/asset-review.json` 协作,不重复发起生成。`game-promotion` 不属于素材生成链路,只能引用已经确认、可追溯的成果。
|
||||
|
||||
这个 Skill 不授予任何 Agent 直接访问 Meowa 的权限。所有角色都必须遵守本地适配器、API Key 隔离、生成元数据、项目内预览和用户审核规则。
|
||||
|
||||
## 每次素材任务的第一步
|
||||
|
||||
先读取当前任务的动态指引;网络不可用时,CLI 会自动使用项目内固定指引:
|
||||
|
||||
```bash
|
||||
MEOWA_GAME_ASSETS_CLI="${NIANCODE_GAME_ASSETS_CLI:-${OPENCODE_CONFIG_DIR}/skills/game-assets/meowart_api.py}"
|
||||
if [ -z "${NIANCODE_PYTHON_PATH:-}" ]; then
|
||||
echo "NianCode managed Python is unavailable; repair or reinstall NianCode." >&2
|
||||
exit 1
|
||||
fi
|
||||
"$NIANCODE_PYTHON_PATH" "$MEOWA_GAME_ASSETS_CLI" skill-doc --task "<这次素材需求>"
|
||||
```
|
||||
|
||||
然后读取项目中的 `GDD.md`、`ASSET_PLAN.md`、`ART_GUIDE.md`、`TASKS.md` 和 `.niancode/asset-review.json`(如果存在)。先判断现有 Works Square、开源素材是否能满足需求;只有在缺口明确时才生成。
|
||||
|
||||
## 生成前检查
|
||||
|
||||
```bash
|
||||
"$NIANCODE_PYTHON_PATH" "$MEOWA_GAME_ASSETS_CLI" config-status
|
||||
"$NIANCODE_PYTHON_PATH" "$MEOWA_GAME_ASSETS_CLI" template-info --kind pixel
|
||||
"$NIANCODE_PYTHON_PATH" "$MEOWA_GAME_ASSETS_CLI" template-info --kind hd
|
||||
```
|
||||
|
||||
`template-info` 返回的模版名和字段是当前可用事实。不要凭记忆编造模版名或字段。没有配置 Meowa 时,明确写入 `ASSET_PLAN.md` 的阻塞项,不要求用户把 Key 发到聊天里。
|
||||
`config-status` 用于确认 Main 是否已准备好 Meowa 凭据。未配置时停止生成,把阻塞原因写入 `ASSET_PLAN.md`,且不要要求用户在聊天中提供 Key。
|
||||
|
||||
## 生成命令
|
||||
|
||||
像素素材:
|
||||
|
||||
```bash
|
||||
"$NIANCODE_PYTHON_PATH" "$MEOWA_GAME_ASSETS_CLI" pixel-gen \
|
||||
--template-name "<从 template-info 选择>" \
|
||||
--requirement "<角色/道具/地形和透明背景要求>" \
|
||||
--aspect-ratio 1:1 \
|
||||
--output-dir assets/generated/meowa
|
||||
```
|
||||
|
||||
高清素材:
|
||||
|
||||
```bash
|
||||
"$NIANCODE_PYTHON_PATH" "$MEOWA_GAME_ASSETS_CLI" hd-gen \
|
||||
--template-name "<从 template-info 选择>" \
|
||||
--requirement "<素材要求>" \
|
||||
--output-dir assets/generated/meowa
|
||||
```
|
||||
|
||||
需要参考图时使用一个或多个 `--reference-file <项目相对路径>`。需要特定模型、分辨率、去背景模式或模版 JSON 时,先从动态指引和 `template-info` 确认,再传 `--model-name`、`--resolution`、`--remove-bg-mode`、`--template-config '<JSON>'`。
|
||||
|
||||
CLI 会等待异步任务完成,把输出下载到明确的项目目录,并写入 `submit_response.json`、`final_response.json` 和 `generation.meta.json`。不要只把远程 URL 写进 `ASSET_PLAN.md`,也不要把未下载的输出提交审核。
|
||||
|
||||
## 项目资产与审核规则
|
||||
|
||||
- 默认输出放在 `assets/generated/meowa/<run>/`;每个候选必须有真实存在的 `localPath`、`previewPath`、`coverPath` 或非空 `manifestPaths`。
|
||||
- `generation.meta.json` 必须保留 provider、Skill 版本、模版、提示词、任务 ID、生成时间、条款链接和授权状态。
|
||||
- Meowa 生成物不是 CC0。当前条款下,商业使用取决于 Meowa 账户方案;免费方案按条款限制为非商业使用,付费方案也要在交付前复核当前条款。记录 `https://meowa.ai/tos`,不要把“可生成”写成“已获商业授权”。
|
||||
- 用户没有 `approve` 前,只能写入候选和审核记录,不能当作正式开发素材接入。
|
||||
- 用户 `discard` 或 `replace` 的候选不得再次提交;候选、pending、discarded 和 replace-requested 不得交给游戏开发 Agent 当作已选素材。
|
||||
- 像素素材预览和缩放使用 nearest-neighbor;保持透明背景、画布尺寸、命名和导出格式与 `ART_GUIDE.md` 一致。
|
||||
- 生成结果失败、超时、下载失败或授权无法确认时,写清楚 `BLOCKED`/`TODO` 和备用方案;不要编造成功结果。
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不运行 `npx skills add`、远程 bootstrap、用户级外部 Skill 或未固定版本的 `game-assets`。
|
||||
- 不直接请求 `https://api.meowa.ai`,不把 API Key 放入命令行参数、提示词、项目文件、日志或聊天。
|
||||
- 不把 Meowa 输出标成 CC0、OpenGameArt、Kenney 或 Works Square 素材。
|
||||
- 不为了增加数量而重复生成同一缺口;先记录已有候选和生成理由。
|
||||
42
.opencode/skills/game-assets/meowart_api.md
Normal file
42
.opencode/skills/game-assets/meowart_api.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Meowa game-assets 固定指引
|
||||
|
||||
这是 NianCode 的离线兜底指引。在线时,`skill-doc` 会优先通过 Electron Main 代理读取 Meowa 当前指引;在线指引不可用时才使用本文件。
|
||||
|
||||
本适配器当前只生成 2D 像素/高清光栅素材,不生成 3D 模型。3D 模型请在 `ASSET_PLAN.md` 中记录项目、用户或许可证明确来源,以及 GLB/glTF 文件和预览证据。
|
||||
|
||||
## 调用顺序
|
||||
|
||||
1. `config-status` 确认 Main 是否配置了 Meowa 凭据。
|
||||
2. `template-info --kind pixel` 或 `template-info --kind hd` 读取真实模版。
|
||||
3. 从 GDD、ART_GUIDE 和 ASSET_PLAN 提取一个明确缺口,写成可验证的 requirement。
|
||||
4. 运行 `pixel-gen` 或 `hd-gen`,等待任务结束并下载文件。
|
||||
5. 检查项目内输出和 `generation.meta.json`,再把候选写入 ASSET_PLAN。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
"$NIANCODE_PYTHON_PATH" meowart_api.py config-status
|
||||
"$NIANCODE_PYTHON_PATH" meowart_api.py skill-doc --task "像素角色和攻击帧"
|
||||
"$NIANCODE_PYTHON_PATH" meowart_api.py template-info --kind pixel
|
||||
"$NIANCODE_PYTHON_PATH" meowart_api.py pixel-gen --template-name "<真实模版名>" --requirement "<需求>" --output-dir assets/generated/meowa
|
||||
"$NIANCODE_PYTHON_PATH" meowart_api.py hd-gen --template-name "<真实模版名>" --requirement "<需求>" --output-dir assets/generated/meowa
|
||||
"$NIANCODE_PYTHON_PATH" meowart_api.py credits
|
||||
```
|
||||
|
||||
`pixel-gen` 和 `hd-gen` 是异步任务。成功后 CLI 会将任务响应、最终响应、下载文件和授权元数据保存在同一个 run 目录。下载失败时不要提交审核标记。
|
||||
|
||||
## 远程 API 事实
|
||||
|
||||
NianCode Main 只代理以下固定能力,并负责注入凭据:
|
||||
|
||||
- `GET /api/pixel-gen/template-info`
|
||||
- `POST /api/pixel-gen`
|
||||
- `GET /api/pixel-gen/jobs?id=...`
|
||||
- `GET /api/pixel-gen/jobs/{id}/download`
|
||||
- `GET /api/hd-gen/template-info`
|
||||
- `POST /api/hd-gen`
|
||||
- `GET /api/hd-gen/jobs?id=...`
|
||||
- `GET /api/hd-gen/jobs/{id}/download`
|
||||
- `GET /api/credits/balance`
|
||||
|
||||
Agent 不应直接构造这些远程 URL;只调用项目内的 `meowart_api.py`。
|
||||
392
.opencode/skills/game-assets/meowart_api.py
Normal file
392
.opencode/skills/game-assets/meowart_api.py
Normal file
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NianCode's fixed, keyless adapter for the Meowa game-assets Skill.
|
||||
|
||||
The Meowa credential stays in Electron Main. This CLI talks only to the local
|
||||
NianCode Host API and writes generated output plus provenance metadata into the
|
||||
active project.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
SKILL_VERSION = "2026.06.19.1"
|
||||
DEFAULT_HOST_BASE = "http://127.0.0.1:13210"
|
||||
DEFAULT_OUTPUT_DIR = "assets/generated/meowa"
|
||||
TERMINAL_STATUSES = {"success", "completed", "failure", "failed", "cancelled", "canceled"}
|
||||
|
||||
|
||||
class AdapterError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def host_base() -> str:
|
||||
value = os.environ.get("NIANCODE_HOST_API_BASE_URL", DEFAULT_HOST_BASE).strip()
|
||||
return value.rstrip("/")
|
||||
|
||||
|
||||
def host_token() -> str:
|
||||
token = os.environ.get("NIANCODE_HOST_API_TOKEN", "").strip()
|
||||
if not token:
|
||||
raise AdapterError("NianCode Host API token is unavailable; run this command from a NianCode Agent")
|
||||
return token
|
||||
|
||||
|
||||
def parse_payload(raw: bytes) -> Any:
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
return raw.decode("utf-8", errors="replace")[:500]
|
||||
|
||||
|
||||
def payload_message(payload: Any, fallback: str) -> str:
|
||||
if isinstance(payload, dict):
|
||||
for key in ("error", "message", "detail", "msg"):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()[:500]
|
||||
if isinstance(payload, str) and payload.strip():
|
||||
return payload.strip()[:500]
|
||||
return fallback
|
||||
|
||||
|
||||
def request_json(path: str, method: str = "GET", payload: dict[str, Any] | None = None, timeout: int = 240) -> Any:
|
||||
url = path if path.startswith("http://") or path.startswith("https://") else f"{host_base()}{path}"
|
||||
body = None
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {host_token()}",
|
||||
}
|
||||
if payload is not None:
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
request = Request(url, data=body, headers=headers, method=method)
|
||||
try:
|
||||
with urlopen(request, timeout=timeout) as response:
|
||||
return parse_payload(response.read())
|
||||
except HTTPError as error:
|
||||
payload_value = parse_payload(error.read())
|
||||
raise AdapterError(payload_message(payload_value, f"Host API returned HTTP {error.code}")) from error
|
||||
except URLError as error:
|
||||
raise AdapterError(f"无法连接 NianCode Host API:{error.reason}") from error
|
||||
|
||||
|
||||
def json_dump(path: Path, value: Any) -> None:
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def print_json(value: Any) -> None:
|
||||
print(json.dumps(value, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def cli_path() -> Path:
|
||||
return Path(__file__).resolve()
|
||||
|
||||
|
||||
def command_skill_doc(args: argparse.Namespace) -> int:
|
||||
query = urlencode({key: value for key, value in {"task": args.task, "topic": args.topic}.items() if value})
|
||||
path = "/api/meowa/game-assets/skill-doc"
|
||||
if query:
|
||||
path = f"{path}?{query}"
|
||||
try:
|
||||
payload = request_json(path)
|
||||
content = payload.get("content") if isinstance(payload, dict) else None
|
||||
if isinstance(content, str) and content.strip():
|
||||
print(content)
|
||||
return 0
|
||||
raise AdapterError("Skill 文档响应没有 content")
|
||||
except AdapterError as error:
|
||||
fallback = cli_path().with_name("meowart_api.md")
|
||||
if fallback.is_file():
|
||||
print(f"[WARN] online Meowa guide unavailable; using bundled guide: {error}", file=sys.stderr)
|
||||
print(fallback.read_text(encoding="utf-8"))
|
||||
return 0
|
||||
raise
|
||||
|
||||
|
||||
def command_skill_doc_status(_args: argparse.Namespace) -> int:
|
||||
guide = cli_path().with_name("meowart_api.md")
|
||||
print_json({
|
||||
"skillVersion": SKILL_VERSION,
|
||||
"bundledGuide": str(guide),
|
||||
"bundledGuideExists": guide.is_file(),
|
||||
"remoteGuide": "通过 NianCode Main 代理读取",
|
||||
})
|
||||
return 0
|
||||
|
||||
|
||||
def command_config_status(_args: argparse.Namespace) -> int:
|
||||
print_json(request_json("/api/meowa/game-assets/config"))
|
||||
return 0
|
||||
|
||||
|
||||
def command_template_info(args: argparse.Namespace) -> int:
|
||||
print_json(request_json(f"/api/meowa/game-assets/template-info?kind={args.kind}"))
|
||||
return 0
|
||||
|
||||
|
||||
def command_credits(_args: argparse.Namespace) -> int:
|
||||
print_json(request_json("/api/meowa/game-assets/credits"))
|
||||
return 0
|
||||
|
||||
|
||||
def slug(value: str) -> str:
|
||||
value = re.sub(r"[^A-Za-z0-9_-]+", "-", value.strip()).strip("-_")
|
||||
return value[:72] or "asset"
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
def read_reference_files(paths: list[str]) -> list[dict[str, str]]:
|
||||
references: list[dict[str, str]] = []
|
||||
total = 0
|
||||
for raw_path in paths:
|
||||
path = Path(raw_path).expanduser()
|
||||
if not path.is_file():
|
||||
raise AdapterError(f"参考文件不存在:{path}")
|
||||
data = path.read_bytes()
|
||||
if len(data) > 8 * 1024 * 1024:
|
||||
raise AdapterError(f"参考文件过大:{path}")
|
||||
total += len(data)
|
||||
if total > 16 * 1024 * 1024:
|
||||
raise AdapterError("参考文件总大小超过限制")
|
||||
references.append({
|
||||
"name": path.name,
|
||||
"mimeType": mimetypes.guess_type(path.name)[0] or "application/octet-stream",
|
||||
"dataBase64": base64.b64encode(data).decode("ascii"),
|
||||
})
|
||||
return references
|
||||
|
||||
|
||||
def safe_request_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
result = dict(payload)
|
||||
references = result.get("referenceFiles")
|
||||
if isinstance(references, list):
|
||||
result["referenceFiles"] = [
|
||||
{"name": item.get("name"), "mimeType": item.get("mimeType")}
|
||||
for item in references
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def extension_for(mime_type: str, file_name: str) -> str:
|
||||
suffix = Path(file_name).suffix.lower()
|
||||
if suffix and len(suffix) <= 10:
|
||||
return suffix
|
||||
return {
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/webp": ".webp",
|
||||
"application/zip": ".zip",
|
||||
"audio/mpeg": ".mp3",
|
||||
"audio/wav": ".wav",
|
||||
}.get(mime_type.lower(), ".bin")
|
||||
|
||||
|
||||
def download_job(kind: str, job_id: str, output_index: int | None = None) -> dict[str, Any]:
|
||||
params: dict[str, str] = {"kind": kind, "id": job_id}
|
||||
if output_index is not None:
|
||||
params["outputIndex"] = str(output_index)
|
||||
payload = request_json(f"/api/meowa/game-assets/download?{urlencode(params)}")
|
||||
if not isinstance(payload, dict) or not payload.get("success"):
|
||||
raise AdapterError(payload_message(payload, "Meowa 下载响应无效"))
|
||||
return payload
|
||||
|
||||
|
||||
def generation_payload(args: argparse.Namespace, kind: str) -> dict[str, Any]:
|
||||
template_config: Any = {}
|
||||
if args.template_config:
|
||||
try:
|
||||
template_config = json.loads(args.template_config)
|
||||
except json.JSONDecodeError as error:
|
||||
raise AdapterError(f"template-config 不是有效 JSON:{error}") from error
|
||||
if not isinstance(template_config, dict):
|
||||
raise AdapterError("template-config 必须是 JSON 对象")
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"kind": kind,
|
||||
"templateName": args.template_name,
|
||||
"templateConfig": template_config,
|
||||
"requirement": args.requirement,
|
||||
"jobName": args.job_name,
|
||||
"modelName": args.model_name,
|
||||
"resolution": args.resolution,
|
||||
"aspectRatio": args.aspect_ratio,
|
||||
"temperature": args.temperature,
|
||||
"includeBase64": args.include_base64,
|
||||
"referenceFiles": read_reference_files(args.reference_file),
|
||||
}
|
||||
if kind == "hd":
|
||||
payload.update({
|
||||
"hdRemoveBgMode": args.remove_bg_mode,
|
||||
"projectId": args.project_id,
|
||||
"threadId": args.thread_id,
|
||||
})
|
||||
return payload
|
||||
|
||||
|
||||
def poll_job(kind: str, job_id: str, max_wait: int, poll_interval: float) -> dict[str, Any]:
|
||||
deadline = time.time() + max(max_wait, 1)
|
||||
last: dict[str, Any] = {}
|
||||
while time.time() <= deadline:
|
||||
payload = request_json(f"/api/meowa/game-assets/jobs?{urlencode({'kind': kind, 'id': job_id})}")
|
||||
if not isinstance(payload, dict):
|
||||
raise AdapterError("Meowa 任务响应不是 JSON 对象")
|
||||
last = payload
|
||||
status = str(payload.get("status") or "").strip().lower()
|
||||
print(f"[INFO] meowa job={job_id} status={status or 'unknown'}", file=sys.stderr)
|
||||
if status in TERMINAL_STATUSES:
|
||||
return payload
|
||||
time.sleep(max(poll_interval, 0.2))
|
||||
raise AdapterError(f"Meowa 任务等待超时:{job_id},最后状态 {last.get('status', 'unknown')}")
|
||||
|
||||
|
||||
def command_generate(args: argparse.Namespace, kind: str) -> int:
|
||||
payload = generation_payload(args, kind)
|
||||
output_root = Path(args.output_dir).expanduser()
|
||||
run_dir = output_root / f"{slug(args.template_name)}-{int(time.time())}"
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
started_at = now_iso()
|
||||
|
||||
json_dump(run_dir / "request.meta.json", {
|
||||
"provider": "meowa",
|
||||
"skillVersion": SKILL_VERSION,
|
||||
"kind": kind,
|
||||
"startedAt": started_at,
|
||||
"request": safe_request_payload(payload),
|
||||
})
|
||||
|
||||
try:
|
||||
submit = request_json("/api/meowa/game-assets/generate", method="POST", payload=payload)
|
||||
json_dump(run_dir / "submit_response.json", submit)
|
||||
if not isinstance(submit, dict):
|
||||
raise AdapterError("Meowa 提交响应不是 JSON 对象")
|
||||
job_id = str(submit.get("api_job_id") or submit.get("job_id") or "").strip()
|
||||
if not job_id:
|
||||
raise AdapterError("Meowa 提交响应缺少任务 ID")
|
||||
print(f"[INFO] submitted meowa job={job_id}", file=sys.stderr)
|
||||
|
||||
final = poll_job(kind, job_id, args.max_wait, args.poll_interval)
|
||||
json_dump(run_dir / "final_response.json", final)
|
||||
status = str(final.get("status") or "").strip().lower()
|
||||
if status not in {"success", "completed"}:
|
||||
raise AdapterError(f"Meowa 任务未成功结束:{status or 'unknown'}")
|
||||
|
||||
download = download_job(kind, job_id)
|
||||
file_name = str(download.get("fileName") or f"{job_id}.bin")
|
||||
mime_type = str(download.get("mimeType") or "application/octet-stream")
|
||||
encoded = download.get("dataBase64")
|
||||
if not isinstance(encoded, str) or not encoded:
|
||||
raise AdapterError("Meowa 下载响应缺少文件内容")
|
||||
file_path = run_dir / f"asset{extension_for(mime_type, file_name)}"
|
||||
file_path.write_bytes(base64.b64decode(encoded))
|
||||
|
||||
metadata = {
|
||||
"provider": "meowa",
|
||||
"integration": "niancode-meowa-game-assets",
|
||||
"skillVersion": SKILL_VERSION,
|
||||
"kind": kind,
|
||||
"templateName": args.template_name,
|
||||
"requirement": args.requirement,
|
||||
"jobId": job_id,
|
||||
"generatedAt": now_iso(),
|
||||
"localPath": str(file_path.as_posix()),
|
||||
"mimeType": mime_type,
|
||||
"sourceType": "ai-generated",
|
||||
"licenseStatus": "verify-meowa-plan-before-shipping",
|
||||
"commercialUse": "依 Meowa 当前账户方案和条款确认;不得默认视为 CC0。",
|
||||
"termsUrl": "https://meowa.ai/tos",
|
||||
"upstreamSkill": "https://github.com/Meowa-AI/meowa-skills/tree/main/skills/game-assets",
|
||||
}
|
||||
json_dump(run_dir / "generation.meta.json", metadata)
|
||||
print(f"[INFO] saved_dir={run_dir}", file=sys.stderr)
|
||||
print_json({"success": True, "jobId": job_id, "file": str(file_path.as_posix()), "metadata": str((run_dir / 'generation.meta.json').as_posix())})
|
||||
return 0
|
||||
except Exception as error:
|
||||
json_dump(run_dir / "error.json", {"error": str(error), "failedAt": now_iso()})
|
||||
raise
|
||||
|
||||
|
||||
def add_generation_args(parser: argparse.ArgumentParser, kind: str) -> None:
|
||||
parser.add_argument("--template-name", required=True)
|
||||
parser.add_argument("--requirement", required=True)
|
||||
parser.add_argument("--template-config", default="")
|
||||
parser.add_argument("--job-name", default="")
|
||||
parser.add_argument("--model-name", default="gemini-3.1-flash-image-preview" if kind == "hd" else "")
|
||||
parser.add_argument("--resolution", default="")
|
||||
parser.add_argument("--aspect-ratio", default="1:1")
|
||||
parser.add_argument("--temperature", type=float, default=0.0)
|
||||
parser.add_argument("--include-base64", action="store_true")
|
||||
parser.add_argument("--reference-file", action="append", default=[])
|
||||
parser.add_argument("--output-dir", default=DEFAULT_OUTPUT_DIR)
|
||||
parser.add_argument("--max-wait", type=int, default=900)
|
||||
parser.add_argument("--poll-interval", type=float, default=3.0)
|
||||
if kind == "hd":
|
||||
parser.add_argument("--remove-bg-mode", default="")
|
||||
parser.add_argument("--project-id", default=None)
|
||||
parser.add_argument("--thread-id", default=None)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="NianCode keyless adapter for Meowa game assets")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
skill_doc = subparsers.add_parser("skill-doc")
|
||||
skill_doc.add_argument("--task", default="")
|
||||
skill_doc.add_argument("--topic", default="")
|
||||
skill_doc.set_defaults(handler=command_skill_doc)
|
||||
|
||||
status = subparsers.add_parser("skill-doc-status")
|
||||
status.set_defaults(handler=command_skill_doc_status)
|
||||
|
||||
config = subparsers.add_parser("config-status")
|
||||
config.set_defaults(handler=command_config_status)
|
||||
|
||||
template = subparsers.add_parser("template-info")
|
||||
template.add_argument("--kind", choices=("pixel", "hd"), required=True)
|
||||
template.set_defaults(handler=command_template_info)
|
||||
|
||||
credits = subparsers.add_parser("credits")
|
||||
credits.set_defaults(handler=command_credits)
|
||||
|
||||
for kind in ("pixel", "hd"):
|
||||
generator = subparsers.add_parser(f"{kind}-gen")
|
||||
add_generation_args(generator, kind)
|
||||
generator.set_defaults(handler=lambda args, selected_kind=kind: command_generate(args, selected_kind))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_parser().parse_args()
|
||||
try:
|
||||
return int(args.handler(args))
|
||||
except AdapterError as error:
|
||||
print(f"[ERROR] {error}", file=sys.stderr)
|
||||
return 1
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
print(f"[ERROR] {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user