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
+94 -17
View File
@@ -11,10 +11,10 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { App as AntApp, ConfigProvider, Popover, Progress, Select } from 'antd';
import { SettingOutlined, DownloadOutlined, ThunderboltOutlined } from '@ant-design/icons';
import { SettingOutlined, DownloadOutlined, ThunderboltOutlined, UploadOutlined } from '@ant-design/icons';
import type { ScanResult, ImageMaterial } from '../../src/collector/scan';
import {
buildGeneratePayload, suiteZipUrl,
buildGeneratePayload, suiteZipUrl, uploadImage,
DEFAULT_PLAN, IMAGE_MODEL_OPTIONS, PLATFORM_OPTIONS, PLATFORM_SPECS, STYLE_SET_OPTIONS,
type PlanItem, type SuiteInfo,
} from '../../src/api/client';
@@ -121,6 +121,12 @@ const App: React.FC = () => {
const [descEdit, setDescEdit] = useState('');
const [paramsOpen, setParamsOpen] = useState(false);
// 手动上传图片(补充参考图,独立「upload」分组)
const [uploadedImages, setUploadedImages] = useState<ImageMaterial[]>([]);
const [uploading, setUploading] = useState(false);
const fileRef = useRef<HTMLInputElement | null>(null);
const uploadSeqRef = useRef(0);
// 服务端
const [settings, setSettings] = useState<BackendSettings>({ baseUrl: 'http://127.0.0.1:3300', token: '' });
@@ -131,6 +137,8 @@ const App: React.FC = () => {
const [model, setModel] = useState<string>('wan2.7-image-pro');
/** 用户改写的风格提示词(按风格 id 存,切风格不丢) */
const [stylePrompts, setStylePrompts] = useState<Record<number, string>>({});
/** 生图要求(最高优先级,强制约束,覆盖其他设定) */
const [requirements, setRequirements] = useState('');
const [plan, setPlan] = useState<PlanItem[]>(DEFAULT_PLAN.map(p => ({ ...p })));
const [planSource, setPlanSource] = useState<'default' | 'ai'>('default');
const [planSummary, setPlanSummary] = useState('');
@@ -261,7 +269,7 @@ const App: React.FC = () => {
if (['done', 'partial', 'failed'].includes(s.status)) {
stopPolling();
setGenerating(false);
if (s.status === 'partial') modal.warning({ title: '部分生成失败', content: '可重试或更换风格重新生成', okText: '知道了' });
if (s.status === 'partial') modal.warning({ title: '部分生成失败', content: s.error || '可重试或更换风格重新生成', okText: '知道了' });
if (s.status === 'failed') modal.error({ title: '生成失败', content: s.error || '未知错误', okText: '知道了' });
}
} catch (e) {
@@ -314,8 +322,8 @@ const App: React.FC = () => {
setSuite(null);
try {
const payload = buildGeneratePayload(
result, selectedKeys, editedTexts(),
{ style_set: styleSet, style_prompt: currentStylePrompt, plan: activePlan, platform, model },
allImages, selectedKeys, editedTexts(),
{ style_set: styleSet, style_prompt: currentStylePrompt, requirements: requirements.trim() || null, plan: activePlan, platform, model },
);
const { suite_id } = await send<{ suite_id: string }>('generateSuite', {
baseUrl: settings.baseUrl, token: settings.token, payload,
@@ -343,6 +351,7 @@ const App: React.FC = () => {
sku_variants: skuVariants,
image_stats: result.stats,
platform,
requirements: requirements.trim() || null,
},
});
if (seq !== planSeqRef.current) return; // 规划已被重新采集重置,丢弃过期响应
@@ -398,13 +407,15 @@ const App: React.FC = () => {
setSelectedKeys(next);
};
const groupImages = (groupKey: string): ImageMaterial[] =>
result?.images.filter(i => i.groupKey === groupKey) ?? [];
/** 全部可选图片:采集结果 + 手动上传(上传图单独一组「upload」) */
const allImages: ImageMaterial[] = result ? [...result.images, ...uploadedImages] : uploadedImages;
/** 预览用的全量图序列(主图→SKU→详情,与展示顺序一致) */
const collectedPreviewList: string[] = result
? (['main', 'sku', 'detail'] as const).flatMap(g => groupImages(g).map(i => i.url))
: [];
const groupImages = (groupKey: string): ImageMaterial[] =>
allImages.filter(i => i.groupKey === groupKey);
/** 预览用的全量图序列(主图→SKU→详情→上传,与展示顺序一致) */
const collectedPreviewList: string[] = (['main', 'sku', 'detail', 'upload'] as const)
.flatMap(g => groupImages(g).map(i => i.url));
/** 生成结果的预览序列(仅成功的图) */
const resultPreviewList: string[] = suite ? suite.images.filter(i => i.status === 'ok').map(i => i.url) : [];
@@ -418,6 +429,43 @@ const App: React.FC = () => {
setPlan(prev => prev.map((p, i) => i === idx ? { ...p, count } : p));
};
/** 手动上传:点击触发隐藏的 file input */
const handleUpload = () => fileRef.current?.click();
const onFilesChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? []);
e.target.value = ''; // 清空以便再次选择同名文件
if (files.length === 0) return;
setUploading(true);
const added: ImageMaterial[] = [];
for (const f of files) {
try {
const { url } = await uploadImage(settings.baseUrl, settings.token, f);
added.push({
key: `upload-${String(++uploadSeqRef.current).padStart(3, '0')}`,
groupKey: 'upload',
groupName: '手动上传',
url,
thumbUrl: url,
index: added.length,
type: 'img',
});
} catch (err) {
modal.error({ title: '上传失败', content: `${f.name}${err instanceof Error ? err.message : String(err)}`, okText: '知道了' });
}
}
if (added.length > 0) {
setUploadedImages(prev => [...prev, ...added]);
setSelectedKeys(prev => {
const next = new Set(prev);
added.forEach(a => next.add(a.key));
return next;
});
modal.success({ title: `已上传 ${added.length} 张图片` });
}
setUploading(false);
};
/** 源站图防盗链时的兜底:走服务端图片代理 */
const proxied = (u: string) =>
`${settings.baseUrl.replace(/\/$/, '')}/api/proxy-image?url=${encodeURIComponent(u)}`;
@@ -567,17 +615,34 @@ const App: React.FC = () => {
no="02"
title="采集图片"
className="section-images"
extra={result ? `已选 ${selectedKeys.size} / ${result.images.filter(i => i.type !== 'video').length}` : undefined}
extra={
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
{(result || uploadedImages.length > 0) && (
<span> {selectedKeys.size} / {allImages.filter(i => i.type !== 'video').length}</span>
)}
<button className="btn btn-sm" onClick={handleUpload} disabled={uploading}>
<UploadOutlined /> {uploading ? '上传中…' : '上传图片'}
</button>
</span>
}
>
{!result ? (
<div className="empty"></div>
<input
ref={fileRef}
type="file"
accept="image/*"
multiple
style={{ display: 'none' }}
onChange={onFilesChange}
/>
{allImages.length === 0 ? (
<div className="empty"></div>
) : (
<div className="img-groups">
{['main', 'sku', 'detail'].map(g => groupImages(g).length > 0 && (
{(['main', 'sku', 'detail', 'upload'] as const).map(g => groupImages(g).length > 0 && (
<div key={g} style={{ marginBottom: 10 }}>
<div className="group-head">
<span className="name">
{g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : '详情图'}
{g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : g === 'detail' ? '详情图' : '手动上传'}
</span>
<span className="count">{groupImages(g).length}</span>
<span
@@ -647,6 +712,7 @@ const App: React.FC = () => {
<span className="plan-title">{p.title}</span>
{p.variant_name && <span className="variant-chip">{p.variant_name}</span>}
{p.detail && <span className="plan-detail" title={p.detail}>{p.detail}</span>}
{p.prompt_hint && <span className="plan-detail plan-hint" title={p.prompt_hint}>🎯 {p.prompt_hint}</span>}
</div>
<Stepper value={p.count} onChange={(v) => setPlanCount(idx, v)} />
</div>
@@ -698,6 +764,15 @@ const App: React.FC = () => {
onChange={(e) => setStylePrompts(p => ({ ...p, [styleSet]: e.target.value }))}
/>
</div>
<div className="field">
<label></label>
<textarea
rows={3}
value={requirements}
onChange={(e) => setRequirements(e.target.value)}
placeholder="选填,例如:必须保留商品正面品牌标识;背景必须为纯黑色;不得添加任何文字水印"
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 4 }}>
{generating && (
<div style={{ flex: 1 }}>
@@ -773,7 +848,9 @@ const App: React.FC = () => {
<div style={{ aspectRatio: suite.ratio === '3:4' ? '3/4' : '1', background: 'var(--card-soft)' }} />
)}
{img.status !== 'ok' && <span className="fail-tag">{img.status === 'failed' ? '✗' : '…'}</span>}
<div className="cap">{img.name}</div>
<div className="cap" title={img.error || img.name}>
{img.status === 'failed' && img.error ? img.error : img.name}
</div>
</div>
))}
</div>
@@ -275,6 +275,8 @@
font-size: 12px; color: var(--text-2);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 构图提示(prompt_hint):生图要求的落地处,用主题蓝区分 */
.plan-detail.plan-hint { color: #4f6bed; }
.stepper { display: inline-flex; align-items: center; gap: 0; flex-shrink: 0; }
.step-btn {
width: 24px; height: 24px; border: 1px solid var(--border-strong); background: #fff;
+26 -6
View File
@@ -2,7 +2,7 @@
* 后端 HTTP 客户端 —— 仅 background 使用(有 host_permissions,不受 CORS 约束)。
* 契约对齐 server 端 /api/materials 与 /api/suites。
*/
import type { ScanResult } from '../collector/scan';
import type { ScanResult, ImageMaterial } from '../collector/scan';
export interface MaterialsPayload {
product_id: string | null;
@@ -218,22 +218,23 @@ export interface GeneratePayload {
images: Array<{ url: string; group_key: string; variant_name?: string | null }>;
style_set: number;
style_prompt?: string;
requirements?: string | null;
plan: PlanItem[];
platform: string;
model?: string | null;
}
/** 组装无状态生成请求:编辑后的文本 + 已勾选图片 + 出图方案 */
/** 组装无状态生成请求:编辑后的文本 + 已勾选图片(含手动上传)+ 出图方案 */
export function buildGeneratePayload(
result: ScanResult,
images: ImageMaterial[],
selectedKeys: Set<string>,
texts: GeneratePayload['texts'],
config: { style_set: number; style_prompt?: string; plan: PlanItem[]; platform: string; model?: string | null },
config: { style_set: number; style_prompt?: string; requirements?: string | null; plan: PlanItem[]; platform: string; model?: string | null },
): GeneratePayload {
const images = result.images
const selected = images
.filter((img) => selectedKeys.has(img.key))
.map((img) => ({ url: img.url, group_key: img.groupKey, variant_name: img.variantName ?? null }));
return { texts, images, ...config };
return { texts, images: selected, ...config };
}
/** 出图方案规划请求体 */
@@ -242,6 +243,7 @@ export interface PlanPayload {
sku_variants: string[];
image_stats: Record<string, number>;
platform: string;
requirements?: string | null;
}
/** AI 智能规划:DeepSeek 根据商品信息生成出图方案 */
@@ -289,3 +291,21 @@ export async function getSuite(baseUrl: string, token: string, suiteId: string):
export function suiteZipUrl(baseUrl: string, suiteId: string): string {
return `${baseUrl.replace(/\/$/, '')}/api/suites/${suiteId}/zip`;
}
/** 手动上传本地图片到服务端,返回可访问 URL(补充参考图用) */
export async function uploadImage(
baseUrl: string,
token: string,
file: File,
): Promise<{ url: string; key: string }> {
const form = new FormData();
form.append('file', file);
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/upload-image`, {
method: 'POST',
headers: authHeaders(token), // 不显式设 Content-Type,交给浏览器生成 boundary
body: form,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.detail || `上传失败 HTTP ${res.status}`);
return data;
}
+1 -1
View File
@@ -17,7 +17,7 @@ export type TextKind =
| 'sales'
| 'shop';
export type ImageGroupKey = 'main' | 'sku' | 'detail' | 'video';
export type ImageGroupKey = 'main' | 'sku' | 'detail' | 'video' | 'upload';
export type SrcProp =
| 'data-lazyload-src'
+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