- 产品持久化:每完成一个产品立即追加写入 products_pending.jsonl,崩溃不丢已完成产品,finish 读盘合并后统一写模板 - 503 致命错误提前终止:compose/product/seed_shot 端到端识别,提前终止搜索分析,丢弃未完成简报,保留已完成落盘产品直接合成模板 - 模特分配:material_library 合格模特图按任务序号独立随机,同 SPU 多款不再共用同一模特 - 图像网关适配:execution_mode/background 默认不再传入 yunfei 等标准网关,base_url 需带 /v1;429/5xx/空响应退避重试 - Pinterest 分析:删除 term 注入与纯文本降级,失败直接放弃;图片上传前 PIL 完整性校验;suitable_for_print=False 过滤丢弃 - 模板导出:不再产生空白 xlsx,文件名=模板原文件名_已填写;写入前按货号末 3 位升序排序 - 删除对接文档.md,更新 README,gitignore 排除测试产物
91 lines
4.0 KiB
Python
91 lines
4.0 KiB
Python
"""阿里云 OSS 图床:图片压缩(3:4、≥1340×1785、<2MB)+ 上传。
|
||
|
||
- compress_for_oss(image_path, out_path):
|
||
中心裁剪到 3:4 → 缩放/放大到 1340×1785 → JPEG quality 迭代压到 <2MB。
|
||
- upload_to_oss(cfg, local_path, object_key):
|
||
用 oss2 上传到 config.oss 指定的 bucket,返回可访问 URL。
|
||
- build_oss_key(country, timestamp, code, rand4):
|
||
key = {国家}/{时间戳}/{货号}_{4位随机}.jpg(货号=前缀+3位计数,000 起最多 999)
|
||
- oss 配置(config.yaml oss 段):
|
||
oss_bucket / oss_endpoint / oss_key_id / oss_key_secret(或环境变量 OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET)
|
||
"""
|
||
import io
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
from PIL import Image
|
||
|
||
# 目标规格:3:4 宽高比,最小 1340×1785,文件 < 2MB
|
||
TARGET_W, TARGET_H = 1340, 1785
|
||
MAX_BYTES = 2 * 1024 * 1024
|
||
|
||
|
||
def compress_for_oss(image_path: str, out_path: str, max_bytes: int = MAX_BYTES) -> str:
|
||
"""压缩图片到 3:4 / ≥1340×1785 / <2MB(JPEG)。返回输出路径。"""
|
||
with Image.open(image_path) as im:
|
||
im = im.convert("RGB")
|
||
|
||
# 1) 中心裁剪到 3:4
|
||
w, h = im.size
|
||
if w / h > 3 / 4: # 太宽 → 裁左右
|
||
new_w = int(h * 3 / 4)
|
||
x0 = (w - new_w) // 2
|
||
im = im.crop((x0, 0, x0 + new_w, h))
|
||
elif w / h < 3 / 4: # 太高 → 裁上下
|
||
new_h = int(w * 4 / 3)
|
||
y0 = (h - new_h) // 2
|
||
im = im.crop((0, y0, w, y0 + new_h))
|
||
|
||
# 2) 缩放到目标尺寸(≥1340×1785,正好 3:4)
|
||
im = im.resize((TARGET_W, TARGET_H), Image.LANCZOS)
|
||
|
||
# 3) JPEG quality 迭代,保证 <2MB
|
||
out = Path(out_path)
|
||
out.parent.mkdir(parents=True, exist_ok=True)
|
||
for quality in (92, 85, 78, 70, 62, 55, 48, 40):
|
||
buf = io.BytesIO()
|
||
im.save(buf, "JPEG", quality=quality, optimize=True, progressive=True)
|
||
if buf.tell() <= max_bytes:
|
||
out.write_bytes(buf.getvalue())
|
||
return str(out)
|
||
# 全部超限 → 用最低质量兜底(仍可能 >2MB,打印警告)
|
||
out.write_bytes(buf.getvalue())
|
||
print(f"[oss] 警告: {Path(image_path).name} 压缩后仍 {buf.tell()/1024/1024:.1f}MB > 2MB(质量 {quality})")
|
||
return str(out)
|
||
|
||
|
||
def upload_to_oss(cfg: dict, local_path: str, object_key: str) -> Optional[str]:
|
||
"""上传本地文件到 OSS,返回 URL;配置缺失/失败返回 None(不中断)。"""
|
||
bucket = (cfg or {}).get("oss_bucket") or ""
|
||
endpoint = (cfg or {}).get("oss_endpoint") or ""
|
||
key_id = (cfg or {}).get("oss_key_id") or ""
|
||
key_secret = (cfg or {}).get("oss_key_secret") or ""
|
||
if not (bucket and endpoint and key_id and key_secret):
|
||
print("[oss] 配置缺失(config.oss),跳过上传")
|
||
return None
|
||
try:
|
||
import oss2
|
||
# 直连 session:忽略环境代理(挂 VPN 时代理会拦截国内 OSS);
|
||
# oss2.Bucket 的 session 必须是 oss2.Session(内部封装 requests),设其底层 trust_env=False
|
||
_session = oss2.Session()
|
||
try:
|
||
_session.session.trust_env = False
|
||
except AttributeError:
|
||
pass
|
||
auth = oss2.Auth(key_id, key_secret)
|
||
bkt = oss2.Bucket(auth, endpoint, bucket, session=_session)
|
||
with open(local_path, "rb") as f:
|
||
bkt.put_object(object_key, f)
|
||
url = f"https://{bucket}.{endpoint}/{object_key}"
|
||
print(f"[oss] 已上传: {url}")
|
||
return url
|
||
except Exception as e: # noqa: BLE001
|
||
print(f"[oss] 上传失败 {local_path}: {e}")
|
||
return None
|
||
|
||
|
||
def build_oss_key(country: str, timestamp: str, code: str, rand4: str, ext: str = ".jpg") -> str:
|
||
"""对象 key:{国家}/{时间戳}/{货号}_{4位随机}.jpg(如 GB/20260820150945/DG000_Ab3x.jpg)。"""
|
||
safe = lambda s: "".join(c for c in (s or "") if c.isalnum() or c in "-_").strip()
|
||
return f"{safe(country)}/{safe(timestamp)}/{safe(code)}_{safe(rand4)}{ext}"
|