Files
ozon-seller-kit/server/services/prompts/common.py
T
2026-09-27 22:41:33 +08:00

201 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""提示词公共层:与模型家族无关的商品上下文、风格模板、图类型名与文案组件。
各家族模块(alibaba / doubao / gpt / google)只负责"如何对模型说话",
商品信息提炼与风格体系统一在这里维护,避免多处漂移。
"""
from __future__ import annotations
import re
# ── 风格模板(与插件端 STYLE_SET_OPTIONS 对应;提示词可被用户在插件里改写覆盖)───
STYLE_SETS: dict[int, dict] = {
1: {
"name": "北欧极简",
"tone": "Nordic editorial e-commerce style for contextual and marketing images only: warm ivory or light greige base, soft window daylight with a subtle warm practical-light accent, low-saturation natural palette, pale wood and tactile linen details, spacious orderly composition, and at most one or two restrained lifestyle props. Keep the product exact and prominent. When text is requested, reserve a clean top text-safe zone with a clear headline-first hierarchy. Never override a task-mandated pure-white background, no-text, or no-props instruction",
"bg": "",
},
2: {
"name": "清新明亮",
"tone": "Fresh, bright, family-friendly e-commerce style for contextual and marketing images only: crisp white with pale sky-blue, mint, or other accents sampled from the product palette; high-key daylight, luminous but natural color, soft clean shadows, airy mobile-first composition, and a cheerful polished retail mood. When text is requested, use a generous top text-safe zone, one bold headline plus one short supporting line or pill, and small simple accent icons only when they clarify a real feature. Never override a task-mandated pure-white background, no-text, or no-props instruction",
"bg": "",
},
3: {
"name": "高级感深色",
"tone": "Premium dark editorial e-commerce style for contextual and marketing images only: deep charcoal, graphite, or near-black setting; sculpted side and rim light; controlled contrast; realistic material texture and sheen; one restrained tonal prop at most; refined, quiet, high-end mood. Let a single product-derived color accent carry the composition. When text is requested, keep a large high-contrast headline in a deliberate text-safe area and a minimal icon or specification row. Never override a task-mandated pure-white background, no-text, or no-props instruction",
"bg": "",
},
4: {
"name": "暖调生活",
"tone": "Warm, cozy lifestyle e-commerce style for contextual and marketing images only: a believable cream, ivory, or warm-beige home setting; soft golden lamp light balanced with gentle window daylight; tactile bedding, knit, or natural wood texture; comforting and approachable atmosphere. Stage one simple usage story with the product as the unquestioned hero; use only category-relevant props and, if appropriate, a natural hand or person interacting with it. For family or children products, a softly styled nursery or bedroom is suitable. When text is requested, use a top headline zone, bold rounded sans-serif lettering, a short secondary pill or line, and small friendly feature icons. Never override a task-mandated pure-white background, no-text, or no-props instruction",
"bg": "",
},
5: {
"name": "纯净棚拍",
"tone": "Clean conversion-focused e-commerce studio style: seamless pure white or very light neutral background only where the task permits, even frontal softbox lighting with a subtle natural contact shadow, faithful color, crisp edge separation, and product filling the frame at a readable scale. No decorative props, lifestyle scenery, artificial reflections, or visual clutter. If the task explicitly requires copy or specifications, use a precise grid, a clear headline-first hierarchy, and sparse line icons; otherwise show no text. Never alter the product or contradict a task-specific background instruction",
"bg": "",
},
}
# ── 图类型中文名(导出文件名用)───────────────────────────────────────────
TYPE_NAMES_ZH: dict[str, str] = {
"white_bg": "白底主图",
"key_features": "核心卖点图",
"selling_pt": "卖点图",
"material": "材质图",
"lifestyle": "场景展示图",
"multi_scene": "多场景拼图",
"ecommerce_detail": "电商详情图",
"size_chart": "尺寸标注图",
"sku_collection": "SKU合集图",
"custom": "创意图",
}
# ── 图内营销文案渲染规范(各家族共用;语言由平台决定)──────────────────────
TEXT_RENDER = {
"zh": (
"Render concise Chinese marketing text inside the image: main headline max 8 Chinese characters, "
"sub-lines max 12 characters each, font is modern clean sans-serif (Source Han Sans style), "
"high legibility, tasteful typography layout, colors harmonized with the composition. "
"No spelling errors, no garbled characters."
),
"en": (
"Render concise English marketing text inside the image: headline max 5 words, "
"sub-lines max 8 words each, Helvetica Neue style sans-serif, high legibility, "
"tasteful typography layout, colors harmonized with the composition. No spelling errors."
),
"ru": (
"Render concise Russian marketing text inside the image: headline max 4 words, "
"sub-lines max 6 words each, modern clean sans-serif (Inter / PT Sans style), "
"proper Cyrillic typography, high legibility, tasteful layout, colors harmonized with the composition. "
"Translate any Chinese source facts into Russian before rendering. All newly generated visible text "
"must be Russian Cyrillic only: never render Chinese, Hanzi, CJK characters, or Chinese phrases. "
"The sole exception is Chinese text physically printed on the reference product itself, which must remain unchanged. "
"No spelling errors, no mixed latin/cyrillic gibberish."
),
}
def resolve_style(style_set: int, style_prompt: str | None = None) -> dict:
"""用户改写的风格提示词整体覆盖内置模板(tone/bg 整体替换)。"""
if style_prompt and style_prompt.strip():
return {"name": "custom", "tone": style_prompt.strip(), "bg": ""}
return STYLE_SETS.get(style_set, STYLE_SETS[1])
def requirements_block(requirements: str | None) -> str:
"""用户强制要求块:最高优先级、置于提示词最前、覆盖冲突指令(原文保留不翻译)。"""
if requirements and requirements.strip():
return (
"STRICT REQUIREMENTS (highest priority for visual intent; interpret any Chinese source instruction "
"by meaning only and never copy its Hanzi into newly generated text): "
+ requirements.strip().rstrip(".")
+ "."
)
return ""
# ── 商品上下文提炼 ────────────────────────────────────────────────────────
def _shorten(text: str, n: int) -> str:
text = re.sub(r"\s+", " ", (text or "")).strip()
return text[:n]
def _clean_title(title: str) -> str:
"""去掉常见堆砌词,让标题更可读。"""
t = _shorten(title, 60)
return re.sub(r"[【【】】\\[\\]|/]", " ", t).strip()
def build_context(raw: dict, fallback_name: str = "", fallback_desc: str = "") -> dict:
"""从采集数据提炼生图上下文:标题、描述行、卖点列表、参数行。
raw: {title, desc, price, params: [{key, value}], sellingPoints}
"""
# 试算页会同时上传中文采集标题和已回填的俄文标题;后者用于图内可见文案。
title = _clean_title(raw.get("title_ru") or raw.get("title") or fallback_name or "product")
desc = _shorten(raw.get("desc") or fallback_desc or "", 200)
# 卖点:优先显式卖点文本;否则从参数表里挑短而有信息量的键值对
selling_points: list[dict] = []
sp_text = raw.get("sellingPoints") or ""
if sp_text:
for chunk in re.split(r"[;;\n·]+|(?<!\d)\.(?!\d)", sp_text):
c = _shorten(chunk, 20)
if c and len(selling_points) < 5:
selling_points.append({"zh": c, "en": c})
if not selling_points:
for p in (raw.get("params") or [])[:12]:
k, v = _shorten(p.get("key", ""), 10), _shorten(str(p.get("value", "")), 16)
if k and v and k.lower() not in {"货号", "sku", "isbn", "上架时间"}:
selling_points.append({"zh": f"{k} {v}", "en": f"{k} {v}"})
if len(selling_points) >= 5:
break
dimensions = raw.get("dimensions") or {}
known_dimensions: list[tuple[str, object]] = []
if isinstance(dimensions, dict):
known_dimensions = [
(label, dimensions[key]) for key, label in (("length", "length"), ("width", "width"), ("height", "height"))
if dimensions.get(key) is not None
]
unit = (dimensions.get("unit") or "cm") if isinstance(dimensions, dict) else "cm"
dimensions_line = "; ".join(f"{label}: {value} {unit}" for label, value in known_dimensions)
known_labels = [label for label, _ in known_dimensions]
missing_labels = [label for label in ("length", "width", "height") if label not in known_labels]
dimension_annotation_instruction = (
"STRICT MEASUREMENT OUTPUT RULE: render exactly "
+ str(len(known_labels))
+ " measurement arrows and labels, only for: "
+ ", ".join(known_labels)
+ ". The only permitted dimension values are: "
+ dimensions_line
+ ". Do not infer product measurements. Do not draw, label, or show an arrow for: "
+ ", ".join(missing_labels or ["any additional direction"])
+ ", depth, thickness, diameter, or any other unlisted dimension."
if known_dimensions else ""
)
params_line = "; ".join(
f"{p.get('key')}: {p.get('value')}" for p in (raw.get("params") or [])[:8]
)
return {
"title": title,
"title_en": title, # 采集源多为中文标题,英文场景直接用原词避免乱翻译
"desc": desc,
"selling_points": selling_points[:3],
"params_line": "; ".join(filter(None, (params_line, dimensions_line))),
# 俄文图片里不能直接渲染中文参数;保留不含汉字的规格(型号、数字、单位)供尺寸图使用。
"params_line_non_cjk": "; ".join(filter(None, (
"; ".join(
f"{p.get('key')}: {p.get('value')}"
for p in (raw.get("params") or [])[:8]
if not re.search(r"[\u3400-\u9fff]", f"{p.get('key')} {p.get('value')}")
),
dimensions_line,
))),
"price": raw.get("price") or "",
"dimension_annotation_instruction": dimension_annotation_instruction,
}
def selling_point_lines(ctx: dict, lang: str, max_n: int = 3) -> str:
"""卖点列表 → 单行文案(图内 callout/标题用),无卖点返回空串。"""
sps = ctx["selling_points"][:max_n]
if not sps:
return ""
key = "zh" if lang == "zh" else "en"
values = [s[key] for s in sps if s.get(key)]
# 中文卖点是模型理解商品的来源,不应成为俄文图片中的逐字文案。
if lang == "ru":
values = [v for v in values if not re.search(r"[\u3400-\u9fff]", v)]
return "; ".join(values)
def params_line_for_output(ctx: dict, lang: str) -> str:
"""返回可进入图内规格文案的参数;俄文模式剔除含汉字的原始采集参数。"""
return ctx.get("params_line") if lang == "zh" else ctx.get("params_line_non_cjk", "")
def type_name(type_id: str) -> str:
return TYPE_NAMES_ZH.get(type_id, type_id)