v88 功能增强:产品落盘持久化 + 生图网关适配 + 模板导出优化

- 产品持久化:每完成一个产品立即追加写入 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 排除测试产物
This commit is contained in:
2026-08-28 10:28:35 +08:00
parent 685b7b0862
commit 2a96ec0870
28 changed files with 1187 additions and 729 deletions
+3 -6
View File
@@ -180,14 +180,11 @@ class MockBackend:
paths = list(image_paths or [])
return [{
"topic": term,
"concept": f"(启发式兜底)围绕「{term}」做原创{art_style}风格印花",
"motif": motif,
"art_style": art_style,
"color_palette": palette,
"composition": composition,
"suitable_for_print": True,
"negative_prompt": negative,
"image_prompt": (f"{motif}, {art_style}, {palette}, {composition}, "
f"original {art_style} t-shirt print design"),
f"original {art_style} t-shirt print design, "
f"no brand logo, no trademark, no character, no watermark"),
# 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应)
"ref_images": [str(paths[i])] if i < len(paths) else [],
"source": "pinterest",
+83 -67
View File
@@ -318,31 +318,56 @@ def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
# —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)——
# image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接),
# 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD (print-on-demand) T-shirt design analyst.
You receive Pinterest reference images for one search term. For each image, extract the VISUAL CONCEPT
(style, mood, motif, color palette, composition) that makes it appealing, then produce an ORIGINAL
T-shirt print design brief that captures that VIBE WITHOUT copying the image.
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD T-shirt design analyst. Given ONE Pinterest reference image,
judge whether it can inspire a T-shirt print, then write an ORIGINAL design brief
capturing its vibe WITHOUT copying.
Your image_prompt will be sent TOGETHER WITH this reference image to an image
generator, so it must actively override visual imitation.
RULES:
- NEVER copy the image, never reproduce the exact artwork, characters, logos, or any text from it.
- Extract only the abstract style/mood/motif concept as inspiration.
- Produce an original, flat, print-ready design brief (no garment, no model, no background scene).
- COPYRIGHT-SAFE: no brands, no logos, no characters, no celebrities, no real persons, no franchises.
- AVOID: politics, religion, hate, violence, sexual content, alcohol, national flags.
- motif: English, concrete central subject of the print (e.g. "a smiling cat with a fish", "geometric mountain layers").
- art_style: English visual technique (e.g. "clean flat vector", "retro screen print").
- color_palette: English colors (e.g. "sunset orange, cream, dusty blue").
- composition: English layout (e.g. "centered emblem with balanced negative space").
- concept: Chinese, one sentence describing the design idea.
- negative_prompt: what to avoid (real people, likeness, characters, logos, text).
- image_prompt: a COMPLETE, fluent English text-to-image prompt for generating the ORIGINAL flat print
design artwork (the print itself, NOT a garment photo). Describe the motif, art style, colors, layout
and mood in natural English, as a standalone print. Do NOT include garment / shirt / model / mannequin /
background-scene / watermark words. Do NOT mention any size or white-background suffix — a fixed
"small centered print on pure white" suffix will be appended automatically by the system.
1. NO COPYING — never reproduce or closely imitate the reference's artwork,
characters, layout or text. Deliberately change motif, arrangement and/or
palette so the two read as clearly different works sharing only a general
style. Distill inspiration into generic style words (retro, y2k, minimal,
grunge, boho, kawaii...); never imitate an identifiable artist/studio/IP
style.
2. FORBIDDEN — brand logos, trademarks, slogans, mascots, copyrighted
characters, real people/celebrities, movie/game/anime/band IP, lyrics,
even stylized or silhouette versions. Avoid politics, religion, violence,
sexual content, alcohol, drugs, gambling, flags, death/occult themes.
3. FORM — ONE clear central subject with strong graphic composition;
print-ready standalone artwork. ANY colors are fine — rich palettes,
gradients and detailed shading are all acceptable. Photographic references
may be rendered as detailed full-color illustrations, retro badges or
vintage stickers.
Return JSON with the field "designs" (array of objects with keys:
motif, art_style, color_palette, composition, concept, negative_prompt, image_prompt)."""
TEXT: short ORIGINAL English wording (1-6 words) allowed; wrap exact words in
double quotes and demand exact spelling; integrate into composition. Never
reuse/translate reference text; no brand/band/movie names or famous slogans.
When unsure, omit.
suitable_for_print: DEFAULT TRUE for graphics, illustrations, badges, vector
art, typography posters, or prints on mockups (judge only the printed artwork).
FALSE only for: subjectless photo scenery, memes/screenshots/collages,
watermarked or very low-quality images, decor/food/candid photos with no
usable motif. Even when FALSE, still fill all fields so downstream never breaks.
image_prompt = two parts:
1) mandatory opener, e.g.: "Use the attached reference image only as loose
inspiration for overall mood, theme and era — do NOT reproduce, trace,
rearrange, recolor or closely imitate any element, character, layout or
text shown in it."
2) the new design: [central motif] + [style] + [color treatment] +
[composition] + [mood], plus quoted original text if used.
NEVER mention shirts, apparel, models, scenes, sizes, backgrounds or
watermarks — placement is handled externally.
negative_prompt: copy of reference artwork, likenesses, characters, logos,
trademarks, watermark, photorealistic shirt/product mockups, busy background;
add garbled-lettering terms only if your design includes text.
OUTPUT — ONLY valid JSON, no fences:
{"designs":[{"suitable_for_print":<bool>,"negative_prompt":"<str>","image_prompt":"<str>"}]}"""
PINTEREST_ANALYZE_SCHEMA = {
"name": "pinterest_design_briefs",
@@ -354,17 +379,11 @@ PINTEREST_ANALYZE_SCHEMA = {
"items": {
"type": "object",
"properties": {
"image_index": {"type": "integer"},
"motif": {"type": "string"},
"art_style": {"type": "string"},
"color_palette": {"type": "string"},
"composition": {"type": "string"},
"concept": {"type": "string"},
"suitable_for_print": {"type": "boolean"},
"negative_prompt": {"type": "string"},
"image_prompt": {"type": "string"},
},
"required": ["image_index", "motif", "art_style", "color_palette",
"composition", "concept", "negative_prompt", "image_prompt"],
"required": ["suitable_for_print", "negative_prompt", "image_prompt"],
"additionalProperties": False,
},
}
@@ -375,18 +394,10 @@ PINTEREST_ANALYZE_SCHEMA = {
}
def build_pinterest_analyze_user_prompt(term: str, country: str, image_count: int) -> str:
def build_pinterest_analyze_user_prompt() -> str:
return (
f"Country: {country}\n"
f"Pinterest search term: {term}\n"
f"Reference images attached: {image_count} images.\n\n"
f"Analyze the attached images and produce {image_count} ORIGINAL design briefs "
f"(one per image), each capturing the visual vibe as an original T-shirt print design. "
f"Do NOT copy the images.\n"
f"For EACH brief you MUST set image_index to the 0-based position of the input image "
f"it was derived from (first image = 0, second = 1, ...). Every image_index from 0 to "
f"{max(image_count - 1, 0)} must appear exactly once — this links each brief to its "
f"source image so the design is generated from the SAME image that was analyzed."
"Analyze the attached image and produce one ORIGINAL T-shirt print design brief "
"that captures its visual vibe without copying it."
)
@@ -587,8 +598,7 @@ class OpenAICompatBackend(LLMBackend):
on_400=None) -> List[Dict[str, Any]]:
"""多模态分析 Pinterest 图片 → 原创设计简报列表。
图片输入不被模型支持(纯文本模型 400)时自动降级纯文本分析(仅用搜索词)
失败返回 [],由节点兜底(回退 mock 规则简报)。
图片输入失败/无有效图片时直接放弃(返回 [],不降级纯文本),由节点跳过该产品
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
"""
cfg = self._cfg
@@ -605,11 +615,26 @@ class OpenAICompatBackend(LLMBackend):
data_uris: List[str] = []
for p in image_paths:
try:
pk = Path(p)
raw = pk.read_bytes()
# 校验图片完整性:损坏/截断的图片会被火山方舟等多模态接口直接 400 拒绝,
# 必须滤掉后才能编码 base64(PIL 打开失败即视为损坏)。
if not raw or len(raw) < 100:
print(f"[pinterest_analyze] 图片文件过小/为空,跳过: {p} ({len(raw)}B)")
continue
try:
from PIL import Image
_im = Image.open(pk)
_im.verify() # 校验文件头/结构,不完整则抛异常
_im.close()
except Exception as _ve: # noqa: BLE001
print(f"[pinterest_analyze] 图片损坏/不完整,跳过: {p} ({_ve})")
continue
import base64 as b64
mime = "image/png"
if Path(p).suffix.lower() in (".jpg", ".jpeg"):
if pk.suffix.lower() in (".jpg", ".jpeg"):
mime = "image/jpeg"
data_uris.append(f"data:{mime};base64,{b64.b64encode(Path(p).read_bytes()).decode()}")
data_uris.append(f"data:{mime};base64,{b64.b64encode(raw).decode()}")
except Exception as e: # noqa: BLE001
print(f"[pinterest_analyze] 图片读取失败 {p}: {e}")
@@ -623,12 +648,11 @@ class OpenAICompatBackend(LLMBackend):
except Exception: # noqa: BLE001
pass
def _call(use_images: bool) -> str:
def _call() -> str:
user_content: List[Any] = [
{"type": "text", "text": build_pinterest_analyze_user_prompt(term, country, len(data_uris))},
{"type": "text", "text": build_pinterest_analyze_user_prompt()},
]
if use_images:
user_content += [{"type": "image_url", "image_url": {"url": u}} for u in data_uris]
user_content += [{"type": "image_url", "image_url": {"url": u}} for u in data_uris]
payload = {
"model": model,
"messages": [
@@ -656,19 +680,15 @@ class OpenAICompatBackend(LLMBackend):
resp.raise_for_status()
return str(resp.json()["choices"][0]["message"].get("content") or "")
raw = ""
if data_uris:
try:
raw = _call(use_images=True)
except Exception as e: # noqa: BLE001 纯文本模型不支持图片 → 降级纯文本
print(f"[pinterest_analyze] 图片输入失败,降级纯文本分析: {e}")
raw = ""
if not raw:
try:
raw = _call(use_images=False)
except Exception as e: # noqa: BLE001
print(f"[pinterest_analyze] 分析失败: {e}")
return []
# 图片输入失败/无有效图片 → 直接放弃该产品(不降级纯文本),由节点跳过后续流程
if not data_uris:
print("[pinterest_analyze] 无有效图片输入,放弃该产品(不降级纯文本)")
return []
try:
raw = _call()
except Exception as e: # noqa: BLE001
print(f"[pinterest_analyze] 图片分析失败,放弃该产品(不降级纯文本): {e}")
return []
try:
parsed = _extract_json(raw)
except Exception as e: # noqa: BLE001
@@ -687,11 +707,7 @@ class OpenAICompatBackend(LLMBackend):
continue
designs.append({
"topic": term,
"concept": str(d.get("concept", "")).strip(),
"motif": str(d.get("motif", "")).strip(),
"art_style": str(d.get("art_style", "")).strip(),
"color_palette": str(d.get("color_palette", "")).strip(),
"composition": str(d.get("composition", "")).strip(),
"suitable_for_print": bool(d.get("suitable_for_print", True)),
"negative_prompt": str(d.get("negative_prompt", "")).strip(),
"image_prompt": str(d.get("image_prompt", "")).strip(),
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)