将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
4.5 KiB
Python
104 lines
4.5 KiB
Python
"""真实出图验证(正式留存版,不带_tmp,勿当临时文件清理)
|
||
用法: cd Clover/backend && python3 scripts/real_image_check.py
|
||
key 只从 ../.env 读,不硬编码不打印明文。
|
||
"""
|
||
from __future__ import annotations
|
||
import asyncio, base64, io, logging, os, sys, time
|
||
from pathlib import Path
|
||
import httpx
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
_env = ROOT / ".env"
|
||
if _env.exists():
|
||
for _l in _env.read_text().splitlines():
|
||
_l = _l.strip()
|
||
if _l and not _l.startswith("#") and "=" in _l:
|
||
k, _, v = _l.partition("=")
|
||
os.environ.setdefault(k.strip(), v.strip())
|
||
|
||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||
logger = logging.getLogger("img")
|
||
|
||
TEST_PROMPT = "\n".join([
|
||
"生成一张小红书可上传的独立竖版3:4图文海报,目标1024x1536,不是App截图。",
|
||
"产品:倍分子素颜霜(美妆护肤·素颜霜)。",
|
||
"画面角色:封面-钩子。画面主体:自然光生活场景,手持素颜霜或产品在桌面前景,像iPhone实拍封面。",
|
||
"图上文字:主标题「黄黑皮逆袭|伪素颜天花板」,点位「黄黑皮也能拥有的妈生好皮」。",
|
||
"核心卖点:黄黑皮提亮、伪素颜自然、不假白不卡纹、洗面奶就能卸、懒人必备。",
|
||
"成分:烟酰胺(改善暗沉)、水解珍珠(锁水保湿)、角鲨烷(不卡纹)。",
|
||
"视觉风格:小红书种草风,明亮干净,暖色调,模拟iPhone主摄浅景深。",
|
||
"重要限制:禁止生成App截图/界面元素;禁止肤色变白/功效前后对比;"
|
||
"避免美白/祛斑/速效/医用等违规词;必须保持产品包装与参考图一致。",
|
||
])
|
||
|
||
_REF = ROOT / "test/output/01_连通测试.png"
|
||
|
||
|
||
async def _apiports(key, url, ref, model, size):
|
||
payload = {"model": model, "prompt": TEST_PROMPT, "n": 1, "size": size,
|
||
"image": base64.b64encode(ref).decode()}
|
||
async with httpx.AsyncClient(timeout=300.0) as c:
|
||
r = await c.post(url, json=payload, headers={"Authorization": f"Bearer {key}"})
|
||
if r.status_code != 200:
|
||
raise RuntimeError(f"HTTP {r.status_code}: {r.text[:200]}")
|
||
return _parse(r.json())
|
||
|
||
|
||
async def _codeproxy(key, base, ref, model, size):
|
||
files = [("model", (None, model)), ("prompt", (None, TEST_PROMPT)),
|
||
("size", (None, size)), ("n", (None, "1")),
|
||
("image[]", ("ref.png", io.BytesIO(ref), "image/png"))]
|
||
async with httpx.AsyncClient(timeout=300.0) as c:
|
||
r = await c.post(f"{base}/images/edits", files=files,
|
||
headers={"Authorization": f"Bearer {key}"})
|
||
if r.status_code != 200:
|
||
raise RuntimeError(f"HTTP {r.status_code}: {r.text[:200]}")
|
||
return _parse(r.json())
|
||
|
||
|
||
def _parse(j):
|
||
item = (j.get("data") or [{}])[0]
|
||
if "b64_json" in item:
|
||
return base64.b64decode(item["b64_json"])
|
||
if "url" in item:
|
||
rr = httpx.get(item["url"], timeout=30.0); rr.raise_for_status()
|
||
return rr.content
|
||
raise ValueError(f"无法解析: {list(item.keys())}")
|
||
|
||
|
||
async def run():
|
||
model = os.environ.get("MODEL_IMAGE", "gpt-image-2")
|
||
size = os.environ.get("IMAGE_SIZE", "1024x1536")
|
||
primary = os.environ.get("IMAGE_PROVIDER_PRIMARY", "apiports").lower()
|
||
fallback = os.environ.get("IMAGE_PROVIDER_FALLBACK", "codeproxy").lower()
|
||
out_dir = Path(__file__).parent.parent / "verify_output"
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
ref = _REF.read_bytes()
|
||
t0 = time.time(); img = None; used = None
|
||
for prov in [primary, fallback]:
|
||
key = os.environ.get(f"{prov.upper()}_KEY", "")
|
||
url = os.environ.get(f"{prov.upper()}_BASE_URL", "")
|
||
if not key or not url:
|
||
continue
|
||
logger.info("[%s] 出图中 model=%s size=%s", prov, model, size)
|
||
try:
|
||
img = await (_apiports(key, url, ref, model, size) if prov == "apiports"
|
||
else _codeproxy(key, url, ref, model, size))
|
||
used = prov; break
|
||
except Exception as e:
|
||
logger.error("[%s] 失败: %s", prov, e)
|
||
dt = time.time() - t0
|
||
print("\n===== 出图结果 =====")
|
||
if img:
|
||
out = out_dir / "素颜霜_封面hook.png"
|
||
out.write_bytes(img)
|
||
print(f" provider: {used}\n 耗时: {dt:.1f}s\n 大小: {len(img)//1024}KB\n 路径: {out}")
|
||
else:
|
||
print(f" 耗时: {dt:.1f}s\n 状态: 全失败")
|
||
print("====================\n")
|
||
return img is not None
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(0 if asyncio.run(run()) else 1)
|