feat: deepseek 的一些修改
This commit is contained in:
@@ -92,6 +92,7 @@ async def generate_suite(
|
||||
product_id=None,
|
||||
style_set=req.style_set,
|
||||
style_prompt=req.style_prompt,
|
||||
requirements=req.requirements,
|
||||
platform=req.platform,
|
||||
lang=spec["lang"],
|
||||
ratio=spec["ratio"],
|
||||
@@ -125,7 +126,7 @@ async def plan_suite(req: PlanRequest) -> PlanResponse:
|
||||
if not product_info.get("title"):
|
||||
raise HTTPException(status_code=400, detail="缺少商品标题,无法规划")
|
||||
try:
|
||||
result = await generate_plan(product_info, req.sku_variants, req.image_stats, req.platform)
|
||||
result = await generate_plan(product_info, req.sku_variants, req.image_stats, req.platform, requirements=req.requirements)
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
except Exception as exc: # noqa: BLE001
|
||||
|
||||
@@ -82,6 +82,7 @@ async def create_suite(
|
||||
suite = Suite(
|
||||
product_id=product.id,
|
||||
style_set=req.style_set,
|
||||
requirements=req.requirements,
|
||||
platform=req.platform,
|
||||
lang=spec["lang"],
|
||||
ratio=spec["ratio"],
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""手动上传图片:插件用户在采集区手动补充参考图,转存本地 media 供预览与生图。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, File, HTTPException, UploadFile
|
||||
|
||||
from services import storage
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["upload"])
|
||||
|
||||
# content-type → 落盘扩展名
|
||||
_ALLOWED_TYPES = {
|
||||
"image/jpeg": ".jpg",
|
||||
"image/png": ".png",
|
||||
"image/webp": ".webp",
|
||||
"image/gif": ".gif",
|
||||
}
|
||||
|
||||
MAX_BYTES = 20 * 1024 * 1024 # 20MB
|
||||
|
||||
|
||||
@router.post("/upload-image")
|
||||
async def upload_image(file: UploadFile = File(...)):
|
||||
data = await file.read()
|
||||
ctype = (file.content_type or "").split(";")[0].strip().lower()
|
||||
if ctype not in _ALLOWED_TYPES:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的图片类型: {file.content_type}")
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="空文件")
|
||||
if len(data) > MAX_BYTES:
|
||||
raise HTTPException(status_code=400, detail="图片超过 20MB")
|
||||
key = storage.write_bytes(data, key_prefix="uploads", ext=_ALLOWED_TYPES[ctype])
|
||||
return {"url": storage.public_url(key), "key": key}
|
||||
@@ -45,6 +45,8 @@ async def _migrate(conn) -> None:
|
||||
cols = {row[1] for row in rows}
|
||||
if "model" not in cols:
|
||||
await conn.execute(text("ALTER TABLE suites ADD COLUMN model VARCHAR(64)"))
|
||||
if "requirements" not in cols:
|
||||
await conn.execute(text("ALTER TABLE suites ADD COLUMN requirements TEXT"))
|
||||
|
||||
|
||||
async def init_db() -> None:
|
||||
|
||||
+2
-1
@@ -8,7 +8,7 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from api import collection, generate, products, proxy, suites
|
||||
from api import collection, generate, products, proxy, suites, upload
|
||||
from config import get_settings
|
||||
from db import init_db
|
||||
from services.storage import media_root
|
||||
@@ -36,6 +36,7 @@ app.include_router(products.router)
|
||||
app.include_router(suites.router)
|
||||
app.include_router(generate.router)
|
||||
app.include_router(proxy.router)
|
||||
app.include_router(upload.router)
|
||||
|
||||
# 静态托管生成的图片/转存素材
|
||||
app.mount("/media", StaticFiles(directory=str(media_root())), name="media")
|
||||
|
||||
@@ -86,6 +86,7 @@ class Suite(Base):
|
||||
status: Mapped[str] = mapped_column(String(16), default=SUITE_PENDING, index=True)
|
||||
style_set: Mapped[int] = mapped_column(Integer, default=1) # 风格模板 1-5
|
||||
style_prompt: Mapped[str | None] = mapped_column(Text, nullable=True) # 用户改写的风格提示词(覆盖模板)
|
||||
requirements: Mapped[str | None] = mapped_column(Text, nullable=True) # 生图要求(最高优先级,强制约束)
|
||||
platform: Mapped[str] = mapped_column(String(8), default="cn") # 目标平台 ozon | wb | cn
|
||||
lang: Mapped[str] = mapped_column(String(4), default="zh") # ru / zh(由平台推导)
|
||||
ratio: Mapped[str] = mapped_column(String(8), default="1:1") # 图片比例(由平台推导)
|
||||
|
||||
@@ -68,6 +68,7 @@ class SuiteCreateRequest(BaseModel):
|
||||
platform: str = Field(default="cn", description="目标平台:ozon | wb | cn")
|
||||
provider: str | None = Field(default=None, description="覆盖默认 provider(doubao | tongyi)")
|
||||
model: str | None = Field(default=None, description="覆盖默认生图模型(tongyi: qwen-image-3.0-pro / wan2.7-image-pro)")
|
||||
requirements: str | None = Field(default=None, description="生图要求(最高优先级,强制约束)")
|
||||
|
||||
|
||||
# ── 无状态套图生成(工具流程:请求自带采集数据)──
|
||||
@@ -93,6 +94,7 @@ class GenerateRequest(BaseModel):
|
||||
images: list[GenerateImageItem] = Field(default_factory=list, description="勾选的参考图")
|
||||
style_set: int = Field(default=1, ge=1, le=7)
|
||||
style_prompt: str | None = Field(default=None, description="用户改写的风格提示词(覆盖 style_set 模板)")
|
||||
requirements: str | None = Field(default=None, description="生图要求(最高优先级,强制约束,覆盖其他设定)")
|
||||
types: list[str] = Field(default_factory=list, description="旧参数:无方案时按类型生成")
|
||||
plan: list[PlanItem] | None = Field(default=None, description="出图方案(优先于 types)")
|
||||
platform: str = Field(default="cn", description="目标平台:ozon | wb | cn")
|
||||
@@ -107,6 +109,7 @@ class PlanRequest(BaseModel):
|
||||
sku_variants: list[str] = Field(default_factory=list, description="带图的 SKU 规格名")
|
||||
image_stats: dict = Field(default_factory=dict, description="分组图片数量统计")
|
||||
platform: str = Field(default="cn")
|
||||
requirements: str | None = Field(default=None, description="生图要求(最高优先级,规划方案必须遵循)")
|
||||
|
||||
|
||||
class PlanItemOut(BaseModel):
|
||||
|
||||
@@ -296,6 +296,7 @@ async def run_suite(suite_id: str) -> None:
|
||||
]
|
||||
|
||||
ok, failed = 0, 0
|
||||
failures: list[str] = []
|
||||
for job in jobs:
|
||||
type_id = job["kind"]
|
||||
image_row = SuiteImage(
|
||||
@@ -309,7 +310,7 @@ async def run_suite(suite_id: str) -> None:
|
||||
try:
|
||||
prompt = build_prompt(
|
||||
type_id, ctx, suite.style_set, suite.lang,
|
||||
extra=job, style_prompt=suite.style_prompt,
|
||||
extra=job, style_prompt=suite.style_prompt, requirements=suite.requirements,
|
||||
)
|
||||
if product:
|
||||
refs = await _select_ref_images(db, product.id, type_id)
|
||||
@@ -322,13 +323,22 @@ async def run_suite(suite_id: str) -> None:
|
||||
ok += 1
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.exception("套图 %s 类型 %s 生成失败", suite_id, type_id)
|
||||
image_row.error = str(exc)[:500]
|
||||
err = str(exc)[:500]
|
||||
image_row.error = err
|
||||
failures.append(f"{job.get('title') or type_name(type_id)}:{err[:200]}")
|
||||
failed += 1
|
||||
await db.commit()
|
||||
|
||||
suite.status = SUITE_DONE if failed == 0 else (SUITE_PARTIAL if ok > 0 else SUITE_FAILED)
|
||||
if failed and not ok:
|
||||
suite.error = "全部生成失败,请检查 API Key / 参考图"
|
||||
if failed:
|
||||
uniq = list(dict.fromkeys(failures)) # 去重保序
|
||||
detail = ";".join(uniq[:6])
|
||||
if len(uniq) > 6:
|
||||
detail += f";…等共 {failed} 张失败"
|
||||
if ok == 0:
|
||||
suite.error = f"全部生成失败。{detail}"
|
||||
else:
|
||||
suite.error = f"部分生成失败({failed} 张)。{detail}"
|
||||
from datetime import datetime, timezone
|
||||
suite.finished_at = datetime.now(timezone.utc)
|
||||
if product:
|
||||
|
||||
@@ -44,6 +44,31 @@ SYSTEM_PROMPT = """你是一名资深电商视觉策划。根据商品信息规
|
||||
{"summary":"三色收纳盒全套图","items":[{"kind":"white_bg","title":"主图·粉色","detail":"粉色SKU白底主视觉","prompt_hint":"front view on white background","count":1,"variant_name":"粉色"}]}"""
|
||||
|
||||
|
||||
def _system_prompt_with_requirements(requirements: str | None) -> str:
|
||||
"""把生图要求作为最高优先级约束注入 system prompt(置于规划规则之前)。
|
||||
|
||||
不仅声明优先级,还明确要求把要求落地到每个方案项的 prompt_hint,
|
||||
避免模型只把要求当作背景信息而不影响输出。
|
||||
"""
|
||||
if not (requirements and requirements.strip()):
|
||||
return SYSTEM_PROMPT
|
||||
marker = "\n## 输出硬性约束"
|
||||
idx = SYSTEM_PROMPT.find(marker)
|
||||
if idx < 0:
|
||||
return SYSTEM_PROMPT
|
||||
req = requirements.strip()
|
||||
block = (
|
||||
"\n## 生图要求(最高优先级,硬性约束,覆盖下方所有规划规则与约束)\n"
|
||||
+ req
|
||||
+ "\n\n"
|
||||
+ "规划方案时,必须把上述生图要求落地到每一项:\n"
|
||||
+ "1. 每个方案项的 prompt_hint 必须融入上述要求的关键约束(如要求纯黑背景,则每个 prompt_hint 都要写明 black background);\n"
|
||||
+ "2. title / detail 措辞不得与上述要求矛盾;\n"
|
||||
+ "3. 任何规划规则与上述要求冲突时,一律以本生图要求为准。\n"
|
||||
)
|
||||
return SYSTEM_PROMPT[:idx] + block + SYSTEM_PROMPT[idx:]
|
||||
|
||||
|
||||
def _normalize_items(raw_items: list, sku_variants: list[str]) -> list[dict]:
|
||||
"""清洗模型输出:kind 白名单、count 钳制、variant 必须真实存在。"""
|
||||
items: list[dict] = []
|
||||
@@ -127,18 +152,26 @@ async def generate_plan(
|
||||
sku_variants: list[str],
|
||||
image_stats: dict,
|
||||
platform: str,
|
||||
requirements: str | None = None,
|
||||
) -> dict:
|
||||
"""调用 DeepSeek 生成方案。返回 {summary, items}。"""
|
||||
"""调用 DeepSeek 生成方案。返回 {summary, items}。
|
||||
|
||||
requirements:生图要求,最高优先级注入 system prompt,规划方案必须遵循。
|
||||
"""
|
||||
s = get_settings()
|
||||
if not s.deepseek_api_key:
|
||||
raise RuntimeError("未配置 DEEPSEEK_API_KEY(.env)")
|
||||
|
||||
user_content = json.dumps({
|
||||
user_payload: dict = {
|
||||
"商品信息": product_info, # {title, desc, params:[{key,value}], sellingPoints, price}
|
||||
"SKU规格": sku_variants, # 带图的 SKU 规格名(variant_name 只能从中选)
|
||||
"图片统计": image_stats, # {main: n, sku: n, detail: n}
|
||||
"目标平台": platform, # ozon/wb/cn(决定图内文案语言)
|
||||
}, ensure_ascii=False)
|
||||
}
|
||||
# 生图要求同时在 user 侧强调(与 system prompt 双重约束),确保模型真正遵循
|
||||
if requirements and requirements.strip():
|
||||
user_payload["生图要求(最高优先级,必须体现在每个方案项中)"] = requirements.strip()
|
||||
user_content = json.dumps(user_payload, ensure_ascii=False)
|
||||
|
||||
async with httpx.AsyncClient(timeout=90, verify=False) as client:
|
||||
resp = await client.post(
|
||||
@@ -147,7 +180,7 @@ async def generate_plan(
|
||||
json={
|
||||
"model": s.deepseek_model,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "system", "content": _system_prompt_with_requirements(requirements)},
|
||||
{"role": "user", "content": user_content},
|
||||
],
|
||||
"response_format": {"type": "json_object"},
|
||||
|
||||
@@ -283,12 +283,14 @@ _PROMPT_BUILDERS = {
|
||||
|
||||
|
||||
def build_prompt(type_id: str, ctx: dict, style_set: int, lang: str, extra: dict | None = None,
|
||||
style_prompt: str | None = None) -> str:
|
||||
style_prompt: str | None = None, requirements: str | None = None) -> str:
|
||||
"""构造指定图类型的完整生图 prompt。
|
||||
|
||||
extra: 方案项信息 {title, detail, prompt_hint}——custom 类型必需,
|
||||
预设类型也会把 prompt_hint 作为构图补充注入。
|
||||
style_prompt: 用户改写的风格提示词,覆盖 style_set 内置模板(tone/bg 整体替换)。
|
||||
requirements: 生图要求(最高优先级,强制约束),置于 prompt 最前面,
|
||||
声明覆盖一切冲突指令,用户可在此输入强制要求。
|
||||
"""
|
||||
if style_prompt and style_prompt.strip():
|
||||
style = {"name": "custom", "tone": style_prompt.strip(), "bg": ""}
|
||||
@@ -305,6 +307,15 @@ def build_prompt(type_id: str, ctx: dict, style_set: int, lang: str, extra: dict
|
||||
hint = (extra.get("prompt_hint") or "").strip()
|
||||
if hint:
|
||||
prompt = prompt.rstrip(".") + f". Additional composition guidance: {hint}."
|
||||
# 生图要求:最高优先级,置于最前并声明覆盖冲突指令(用户输入原样保留,不翻译)
|
||||
if requirements and requirements.strip():
|
||||
prompt = (
|
||||
"STRICT REQUIREMENTS (highest priority, must be followed exactly, "
|
||||
"override any conflicting instruction): "
|
||||
+ requirements.strip().rstrip(".")
|
||||
+ ". "
|
||||
+ prompt
|
||||
)
|
||||
return prompt + ". " + DEFAULT_NEGATIVE_INTENT
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user