feat: deepseek 的一些修改

This commit is contained in:
Joey
2026-08-16 22:20:15 +08:00
parent b57933e983
commit 9d15d8f784
14 changed files with 229 additions and 35 deletions
+2 -1
View File
@@ -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
+1
View File
@@ -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"],
+32
View File
@@ -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}
+2
View File
@@ -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
View File
@@ -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")
+1
View File
@@ -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") # 图片比例(由平台推导)
+3
View File
@@ -68,6 +68,7 @@ class SuiteCreateRequest(BaseModel):
platform: str = Field(default="cn", description="目标平台:ozon | wb | cn")
provider: str | None = Field(default=None, description="覆盖默认 providerdoubao | 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):
+14 -4
View File
@@ -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:
+37 -4
View File
@@ -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"},
+12 -1
View File
@@ -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