完善小红书卡片与 GitHub 交付流程
This commit is contained in:
@@ -42,7 +42,8 @@ STYLE_PRESETS: Dict[str, str] = {
|
||||
"白底纸感、手绘箭头、便利贴、重点圈注和轻量猫咪贴纸,适合避坑经验和保姆式步骤。"
|
||||
),
|
||||
"xiaohongshu-vertical": (
|
||||
"9:16 手机端可读,大字少字,强标题钩子,4 图系列一致视觉,带 MAOMOMO 标识。"
|
||||
"3:4 或 9:16 原生竖版,手机端可读,大字少字,强标题钩子,多图系列一致视觉,"
|
||||
"默认 2D 扁平信息卡片,用户确认后可用 3D 轻拟物风,带 MAOMOMO 标识。"
|
||||
),
|
||||
"clean-professional": (
|
||||
"浅色背景、蓝绿或暖橙强调、结构化信息卡片和清晰层级,适合正式银行规则、开户教程和综合攻略。"
|
||||
@@ -267,6 +268,8 @@ def build_prompt(
|
||||
core_text: str,
|
||||
style: str,
|
||||
aspect_ratio: str,
|
||||
visual_direction: str = "",
|
||||
portrait_rules: str = "",
|
||||
extra: str = "",
|
||||
) -> str:
|
||||
allowed_visible = [value for value in (title, core_text, "MAOMOMO") if value.strip()]
|
||||
@@ -281,6 +284,15 @@ def build_prompt(
|
||||
f"可见文字限制:画面只允许出现这些指定业务文案:{allowed_visible_text};如补充要求明确列出已核对的品牌或支付标识,可作为小型辅助标识出现;不要添加副标题、风格标签、prompt 描述、说明文字、额外 slogan 或无关文字。",
|
||||
"硬性要求:包含清晰可见的 MAOMOMO 主标识;中文文字清楚可读;信息层级明确;港币金额统一使用 HKD 写法;不要伪造真实 App 截图;Mastercard、Visa、UnionPay、银行、支付宝等 Logo 或支付网络标识只有在官方材料、用户素材或补充要求明确核对正确时才可出现,并且只能作为辅助识别,不能抢占或替代 MAOMOMO 主标识位置;未确认时使用通用卡片或通用支付符号,不要凭空加入或替换卡组织;不要出现真实个人信息;猫咪不穿戴第三方 Logo。",
|
||||
]
|
||||
if visual_direction.strip():
|
||||
parts.append(f"确认视觉方向:{visual_direction.strip()}。整组图片必须保持同一视觉方向,不得混用 2D 和 3D。")
|
||||
if style == "xiaohongshu-vertical" or aspect_ratio in {"3:4", "9:16"}:
|
||||
rules = portrait_rules.strip() or (
|
||||
"native portrait composition;no squeezed elements;"
|
||||
"no stretched card / logo / text;vertical layout redesigned for Xiaohongshu;"
|
||||
"禁止把横版设计硬塞进竖版画幅。"
|
||||
)
|
||||
parts.append(f"小红书竖图要求:{rules}")
|
||||
if extra.strip():
|
||||
parts.append(f"补充要求:{extra.strip()}")
|
||||
return "\n".join(parts)
|
||||
@@ -352,6 +364,8 @@ def generate_one(args: argparse.Namespace) -> Dict[str, str]:
|
||||
core_text=args.core_text,
|
||||
style=args.style,
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
visual_direction=args.visual_direction,
|
||||
portrait_rules=args.portrait_rules,
|
||||
extra=args.extra,
|
||||
)
|
||||
|
||||
@@ -393,6 +407,12 @@ def batch(args: argparse.Namespace) -> List[Dict[str, str]]:
|
||||
die(f"manifest 第 {index} 项缺少 file_name。")
|
||||
style = str(item.get("style") or args.style or DEFAULT_STYLE)
|
||||
aspect_ratio = str(item.get("aspect_ratio") or args.aspect_ratio)
|
||||
visual_direction = str(item.get("visual_direction") or args.visual_direction or "")
|
||||
raw_portrait_rules = item.get("portrait_composition_rules") or args.portrait_rules
|
||||
if isinstance(raw_portrait_rules, list):
|
||||
portrait_rules = ";".join(str(value) for value in raw_portrait_rules)
|
||||
else:
|
||||
portrait_rules = str(raw_portrait_rules or "")
|
||||
prompt = item.get("prompt")
|
||||
if not isinstance(prompt, str) or not prompt.strip():
|
||||
prompt = build_prompt(
|
||||
@@ -401,6 +421,8 @@ def batch(args: argparse.Namespace) -> List[Dict[str, str]]:
|
||||
core_text=str(item.get("core_text") or item.get("caption") or item.get("alt_text") or ""),
|
||||
style=style,
|
||||
aspect_ratio=aspect_ratio,
|
||||
visual_direction=visual_direction,
|
||||
portrait_rules=portrait_rules,
|
||||
extra=str(item.get("extra") or item.get("requirements") or ""),
|
||||
)
|
||||
|
||||
@@ -439,6 +461,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
common.add_argument("--quality", default=DEFAULT_QUALITY)
|
||||
common.add_argument("--output-format", default="png")
|
||||
common.add_argument("--aspect-ratio", default="16:9")
|
||||
common.add_argument("--visual-direction", default="")
|
||||
common.add_argument("--portrait-rules", default="")
|
||||
common.add_argument("--dry-run", action="store_true", help="Only write prompt files; do not call the image API.")
|
||||
|
||||
generate = subparsers.add_parser("generate", parents=[common], help="Generate one image.")
|
||||
|
||||
@@ -17,6 +17,8 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
JOBS_FILE = "slide_jobs.json"
|
||||
STATE_FILE = "slide_run_state.json"
|
||||
FINAL_MANIFEST_FILE = "final_manifest.json"
|
||||
GITHUB_BATCHES_FILE = "github_batches.json"
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
@@ -64,6 +66,13 @@ def resolve_in_project(project: Path, value: str) -> Path:
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def relative_path_string(project: Path, path: Path) -> str:
|
||||
try:
|
||||
return str(path.resolve().relative_to(project))
|
||||
except ValueError:
|
||||
return str(path.resolve())
|
||||
|
||||
|
||||
def load_job_doc(project: Path) -> Dict[str, Any]:
|
||||
raw = read_json(project / JOBS_FILE)
|
||||
if isinstance(raw, list):
|
||||
@@ -88,6 +97,18 @@ def load_state_doc(project: Path) -> Dict[str, Any]:
|
||||
return raw
|
||||
|
||||
|
||||
def load_batches_doc(project: Path) -> Dict[str, Any]:
|
||||
path = project / GITHUB_BATCHES_FILE
|
||||
if not path.exists():
|
||||
return {"version": 1, "batches": []}
|
||||
raw = read_json(path)
|
||||
if not isinstance(raw, dict):
|
||||
die(f"{GITHUB_BATCHES_FILE} 必须是对象。")
|
||||
if not isinstance(raw.get("batches"), list):
|
||||
raw["batches"] = []
|
||||
return raw
|
||||
|
||||
|
||||
def save_docs(project: Path, job_doc: Dict[str, Any], state_doc: Dict[str, Any]) -> None:
|
||||
write_json(project / JOBS_FILE, job_doc)
|
||||
write_json(project / STATE_FILE, state_doc)
|
||||
@@ -159,6 +180,14 @@ def cmd_init(args: argparse.Namespace) -> int:
|
||||
"asset_out": infer_asset_out(data, origin_out),
|
||||
"status": "pending",
|
||||
"selected_backend": args.selected_backend,
|
||||
"aspect_ratio": data.get("aspect_ratio", ""),
|
||||
"visual_direction": data.get("visual_direction", ""),
|
||||
"delivery_batch": data.get("delivery_batch", ""),
|
||||
"qa_status": "pending",
|
||||
"entered_assets": False,
|
||||
"final": False,
|
||||
"github_pushed": False,
|
||||
"github_batch": "",
|
||||
"created_at": now_iso(),
|
||||
}
|
||||
)
|
||||
@@ -186,7 +215,9 @@ def cmd_status(args: argparse.Namespace) -> int:
|
||||
print(json.dumps({"project": str(project), "counts": counts}, ensure_ascii=False, indent=2))
|
||||
for job in job_doc["jobs"]:
|
||||
if isinstance(job, dict):
|
||||
print(f"{job_key(job) or '<unknown>'}\t{job.get('status', 'unknown')}\t{job.get('origin_out', '')}")
|
||||
github = "github" if job.get("github_pushed") else "local"
|
||||
qa = job.get("qa_status", "")
|
||||
print(f"{job_key(job) or '<unknown>'}\t{job.get('status', 'unknown')}\t{qa}\t{github}\t{job.get('origin_out', '')}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -238,19 +269,25 @@ def cmd_result(args: argparse.Namespace) -> int:
|
||||
copy_if_needed(source, origin_out)
|
||||
|
||||
asset_value = args.asset_out or job.get("asset_out")
|
||||
entered_assets = False
|
||||
if isinstance(asset_value, str) and asset_value:
|
||||
copy_if_needed(origin_out, resolve_in_project(project, asset_value))
|
||||
entered_assets = True
|
||||
|
||||
result = {
|
||||
"agent_id": args.agent_id,
|
||||
"backend_used": args.backend_used,
|
||||
"selected_source": str(source),
|
||||
"origin_out": str(origin_out.relative_to(project) if origin_out.is_relative_to(project) else origin_out),
|
||||
"origin_out": relative_path_string(project, origin_out),
|
||||
"asset_out": asset_value,
|
||||
"qa_status": "passed",
|
||||
"entered_assets": entered_assets,
|
||||
"qa_note": args.qa_note,
|
||||
"time": now_iso(),
|
||||
}
|
||||
job["status"] = "recorded"
|
||||
job["qa_status"] = "passed"
|
||||
job["entered_assets"] = entered_assets
|
||||
job["result"] = result
|
||||
event(state_doc, "result", args.job, result)
|
||||
save_docs(project, job_doc, state_doc)
|
||||
@@ -277,6 +314,177 @@ def cmd_blocker(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_final_manifest(args: argparse.Namespace) -> int:
|
||||
project = project_path(args.project)
|
||||
job_doc = load_job_doc(project)
|
||||
state_doc = load_state_doc(project)
|
||||
assets: List[Dict[str, Any]] = []
|
||||
incomplete: List[str] = []
|
||||
|
||||
for job in job_doc["jobs"]:
|
||||
if not isinstance(job, dict):
|
||||
continue
|
||||
key = job_key(job) or "<unknown>"
|
||||
if job.get("status") != "recorded":
|
||||
incomplete.append(f"{key}: status={job.get('status', 'unknown')}")
|
||||
continue
|
||||
if job.get("qa_status") != "passed":
|
||||
incomplete.append(f"{key}: qa_status={job.get('qa_status', 'unknown')}")
|
||||
continue
|
||||
if job.get("entered_assets") is not True:
|
||||
incomplete.append(f"{key}: entered_assets=false")
|
||||
continue
|
||||
|
||||
result = job.get("result") if isinstance(job.get("result"), dict) else {}
|
||||
asset_value = result.get("asset_out") or job.get("asset_out")
|
||||
if not isinstance(asset_value, str) or not asset_value:
|
||||
incomplete.append(f"{key}: 缺少 asset_out")
|
||||
continue
|
||||
|
||||
asset_path = resolve_in_project(project, asset_value)
|
||||
if not asset_path.exists():
|
||||
incomplete.append(f"{key}: assets 文件不存在:{asset_value}")
|
||||
continue
|
||||
|
||||
job["final"] = True
|
||||
assets.append(
|
||||
{
|
||||
"id": key,
|
||||
"prompt_file": job.get("prompt_file", ""),
|
||||
"origin_out": result.get("origin_out") or job.get("origin_out", ""),
|
||||
"asset_out": relative_path_string(project, asset_path),
|
||||
"qa_status": job.get("qa_status", ""),
|
||||
"qa_note": result.get("qa_note", ""),
|
||||
"aspect_ratio": job.get("aspect_ratio", ""),
|
||||
"visual_direction": job.get("visual_direction", ""),
|
||||
"delivery_batch": job.get("delivery_batch", ""),
|
||||
"github_pushed": bool(job.get("github_pushed")),
|
||||
"github_batch": job.get("github_batch", ""),
|
||||
}
|
||||
)
|
||||
|
||||
if incomplete and not args.allow_incomplete:
|
||||
die("仍有未进入最终 manifest 的图片 job:\n" + "\n".join(incomplete))
|
||||
if not assets:
|
||||
die("没有可写入 final_manifest.json 的最终图片。")
|
||||
|
||||
manifest_path = resolve_in_project(project, args.out)
|
||||
manifest = {
|
||||
"version": 1,
|
||||
"generated_at": now_iso(),
|
||||
"project": project.name,
|
||||
"asset_count": len(assets),
|
||||
"assets": assets,
|
||||
}
|
||||
write_json(manifest_path, manifest)
|
||||
event(
|
||||
state_doc,
|
||||
"final_manifest",
|
||||
"*",
|
||||
{
|
||||
"path": relative_path_string(project, manifest_path),
|
||||
"asset_count": len(assets),
|
||||
"allow_incomplete": args.allow_incomplete,
|
||||
},
|
||||
)
|
||||
save_docs(project, job_doc, state_doc)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "final_manifest_written",
|
||||
"path": relative_path_string(project, manifest_path),
|
||||
"assets": len(assets),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_github_batch(args: argparse.Namespace) -> int:
|
||||
project = project_path(args.project)
|
||||
job_doc = load_job_doc(project)
|
||||
state_doc = load_state_doc(project)
|
||||
batches_doc = load_batches_doc(project)
|
||||
|
||||
rel_paths: List[str] = []
|
||||
for value in args.paths:
|
||||
path = resolve_in_project(project, value)
|
||||
if not path.exists():
|
||||
die(f"批次文件不存在:{value}")
|
||||
rel_paths.append(relative_path_string(project, path))
|
||||
|
||||
path_set = set(rel_paths)
|
||||
pushed_jobs: List[str] = []
|
||||
for job in job_doc["jobs"]:
|
||||
if not isinstance(job, dict):
|
||||
continue
|
||||
result = job.get("result") if isinstance(job.get("result"), dict) else {}
|
||||
candidate_values = [job.get("asset_out"), result.get("asset_out")]
|
||||
normalized = {
|
||||
relative_path_string(project, resolve_in_project(project, value))
|
||||
for value in candidate_values
|
||||
if isinstance(value, str) and value
|
||||
}
|
||||
if normalized.intersection(path_set):
|
||||
job["github_pushed"] = True
|
||||
job["github_batch"] = args.batch
|
||||
if isinstance(job.get("result"), dict):
|
||||
job["result"]["github_pushed"] = True
|
||||
job["result"]["github_batch"] = args.batch
|
||||
pushed_jobs.append(job_key(job) or "<unknown>")
|
||||
|
||||
batch_record = {
|
||||
"batch": args.batch,
|
||||
"commit": args.commit,
|
||||
"url": args.url,
|
||||
"release": args.release,
|
||||
"paths": rel_paths,
|
||||
"pushed_jobs": pushed_jobs,
|
||||
"note": args.note,
|
||||
"time": now_iso(),
|
||||
}
|
||||
batches = [item for item in batches_doc["batches"] if not (isinstance(item, dict) and item.get("batch") == args.batch)]
|
||||
batches.append(batch_record)
|
||||
batches_doc["batches"] = batches
|
||||
|
||||
manifest_path = project / FINAL_MANIFEST_FILE
|
||||
if manifest_path.exists():
|
||||
manifest = read_json(manifest_path)
|
||||
if isinstance(manifest, dict) and isinstance(manifest.get("assets"), list):
|
||||
for item in manifest["assets"]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
asset_value = item.get("asset_out")
|
||||
if not isinstance(asset_value, str) or not asset_value:
|
||||
continue
|
||||
normalized = relative_path_string(project, resolve_in_project(project, asset_value))
|
||||
if normalized in path_set:
|
||||
item["github_pushed"] = True
|
||||
item["github_batch"] = args.batch
|
||||
item["github_commit"] = args.commit
|
||||
item["github_url"] = args.url
|
||||
write_json(manifest_path, manifest)
|
||||
|
||||
write_json(project / GITHUB_BATCHES_FILE, batches_doc)
|
||||
event(state_doc, "github_batch", "*", batch_record)
|
||||
save_docs(project, job_doc, state_doc)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "github_batch_recorded",
|
||||
"batch": args.batch,
|
||||
"paths": len(rel_paths),
|
||||
"pushed_jobs": pushed_jobs,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Record MAOMOMO article image job state.")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
@@ -317,6 +525,22 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
blocker.add_argument("--reason", required=True)
|
||||
blocker.add_argument("--evidence", default="")
|
||||
blocker.set_defaults(func=cmd_blocker)
|
||||
|
||||
final_manifest = subparsers.add_parser("final-manifest", help="Write final_manifest.json from recorded assets.")
|
||||
final_manifest.add_argument("project")
|
||||
final_manifest.add_argument("--out", default=FINAL_MANIFEST_FILE)
|
||||
final_manifest.add_argument("--allow-incomplete", action="store_true")
|
||||
final_manifest.set_defaults(func=cmd_final_manifest)
|
||||
|
||||
github_batch = subparsers.add_parser("github-batch", help="Record a GitHub delivery batch.")
|
||||
github_batch.add_argument("project")
|
||||
github_batch.add_argument("--batch", required=True)
|
||||
github_batch.add_argument("--commit", required=True)
|
||||
github_batch.add_argument("--url", required=True)
|
||||
github_batch.add_argument("--release", default="")
|
||||
github_batch.add_argument("--paths", nargs="+", required=True)
|
||||
github_batch.add_argument("--note", default="")
|
||||
github_batch.set_defaults(func=cmd_github_batch)
|
||||
return parser
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user