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:
@@ -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 按图逐张产出简报,顺序一一对应)
|
||||
|
||||
Reference in New Issue
Block a user