fix: gate AI character safety words

This commit is contained in:
2026-05-26 10:23:00 +08:00
parent a3a9ed90e2
commit daec523dec
2 changed files with 75 additions and 8 deletions

View File

@@ -5308,7 +5308,7 @@ def _translate_text_sync(text: str, target: Literal["en", "zh"] = "en", *, max_t
return text return text
target_label = "English" if target == "en" else "Simplified Chinese" target_label = "English" if target == "en" else "Simplified Chinese"
prompt = ( prompt = (
f"Translate the following TikTok ad planning text into concise natural {target_label}. " f"Translate the following creative generation planning text into concise natural {target_label}. "
"Preserve concrete product, camera, subject, timing, and structure details. " "Preserve concrete product, camera, subject, timing, and structure details. "
"Do not add commentary, markdown, quotes, or explanations.\n\n" "Do not add commentary, markdown, quotes, or explanations.\n\n"
f"Input:\n{text}" f"Input:\n{text}"
@@ -5412,21 +5412,75 @@ def _parse_creative_copy_response(raw: str, req: CreativeCopyReq) -> CreativeCop
return CreativeCopyResp(model=REWRITE_MODEL if LLM_API_KEY else "fallback", variants=variants) return CreativeCopyResp(model=REWRITE_MODEL if LLM_API_KEY else "fallback", variants=variants)
_NO_PERSON_INTENT_RE = re.compile(
r"("
r"无人|无人物|没有人|没有人物|不要人|不要人物|不出现人|不出现人物|无行人|无路人|空无一人|"
r"no\s+(?:people|person|human|humans|faces?|characters?|crowd|bystanders)|"
r"without\s+(?:people|person|human|humans|faces?|characters?|crowd|bystanders)|"
r"empty\s+(?:scene|street|room|space)"
r")",
re.I,
)
_PERSON_INTENT_RE = re.compile(
r"("
r"人物|人像|真人|模特|男生|女生|男人|女人|男士|女士|女孩|男孩|少年|少女|青年|"
r"主播|演员|艺人|角色|数字人|虚拟人|博主|网红|行人|路人|人群|背影|侧脸|正脸|"
r"面部|脸部|脸|表情|肖像|全身|半身|"
r"\b(?:person|people|human|humans|model|woman|man|girl|boy|male|female|actor|actress|"
r"presenter|host|influencer|character|avatar|portrait|face|facial|full[- ]body|crowd|"
r"pedestrian|bystander|passerby)\b"
r")",
re.I,
)
def _prompt_has_person_intent(*parts: str) -> bool:
text = "\n".join(part for part in parts if part).strip()
if not text or _NO_PERSON_INTENT_RE.search(text):
return False
return bool(_PERSON_INTENT_RE.search(text))
def _prompt_person_guard(req: PromptPolishReq) -> str:
if req.mode not in {"image", "video", "general"}:
return ""
if _prompt_has_person_intent(req.text, req.system_prompt):
return (
"The user requested a person, portrait, model, or character subject. "
"Describe any such subject as a fully fictional synthetic AI character or virtual avatar, "
"not based on any real person, celebrity, public figure, or identifiable private individual. "
"Avoid real-person likeness, biometric identity, endorsement, or impersonation.\n"
)
return (
"The user did not request a person or character subject. Preserve the original object-only, "
"scene-only, or product-only composition. Do not introduce people, faces, bodies, hands, "
"avatars, characters, crowds, bystanders, or human silhouettes.\n"
)
def _prompt_polish_fallback(req: PromptPolishReq) -> PromptPolishResp: def _prompt_polish_fallback(req: PromptPolishReq) -> PromptPolishResp:
text = req.text.strip() text = req.text.strip()
base = _ensure_english(text) if req.target_language == "en" else text base = _ensure_english(text) if req.target_language == "en" else text
base = re.sub(r"\s+", " ", base).strip() base = re.sub(r"\s+", " ", base).strip()
base = re.sub(r"[。.!?]+$", "", base).strip()
person_intent = _prompt_has_person_intent(req.text, req.system_prompt)
person_guard = (
" Use a fully fictional synthetic AI character, not based on any real person, celebrity, public figure, or identifiable private individual."
if person_intent
else " Preserve the original object-only, scene-only, or product-only composition; do not introduce people, faces, bodies, hands, avatars, characters, crowds, bystanders, or human silhouettes."
)
if req.mode == "video": if req.mode == "video":
polished = ( polished = (
f"{base}. Smooth camera movement, clear subject continuity, stable composition, " f"{base}. Smooth camera movement, clear subject continuity, stable composition, "
"natural motion, coherent lighting, no subtitles, no watermark." f"natural motion, coherent lighting, no subtitles, no watermark.{person_guard}"
) )
elif req.mode in {"general", "chat"}: elif req.mode in {"general", "chat"}:
polished = base polished = base
else: else:
polished = ( polished = (
f"{base}. Detailed visual prompt, clear main subject, coherent composition, " f"{base}. Detailed visual prompt, clear main subject, coherent composition, "
"natural lighting, refined color palette, high-quality details." f"natural lighting, refined color palette, high-quality details.{person_guard}"
) )
return PromptPolishResp(model="fallback", text=polished[:1800]) return PromptPolishResp(model="fallback", text=polished[:1800])
@@ -5458,6 +5512,7 @@ def polish_prompt(req: PromptPolishReq) -> PromptPolishResp:
"Do not add medical, wellness, or advertising claims unless the user asked for them.\n" "Do not add medical, wellness, or advertising claims unless the user asked for them.\n"
"Improve concrete visual details, composition, lighting, camera language, materials, mood, and quality.\n" "Improve concrete visual details, composition, lighting, camera language, materials, mood, and quality.\n"
"Return only the rewritten prompt. No markdown, labels, JSON, quotes, explanation, or alternatives.\n" "Return only the rewritten prompt. No markdown, labels, JSON, quotes, explanation, or alternatives.\n"
f"{_prompt_person_guard(req)}"
) )
if req.mode == "chat": if req.mode == "chat":
prompt = ( prompt = (
@@ -5470,7 +5525,7 @@ def polish_prompt(req: PromptPolishReq) -> PromptPolishResp:
if req.mode == "video": if req.mode == "video":
prompt += ( prompt += (
"For video, describe motion, timing, camera movement, continuity, and what changes over time. " "For video, describe motion, timing, camera movement, continuity, and what changes over time. "
"If people are not essential, prefer no recognizable faces or distant/turned-away background people to reduce video safety risk.\n" "Do not add people for scale, atmosphere, lifestyle context, or background decoration unless the input explicitly asked for people.\n"
) )
if user_system: if user_system:
prompt += f"\nUser-selected polishing guidance:\n{user_system[:1000]}\n" prompt += f"\nUser-selected polishing guidance:\n{user_system[:1000]}\n"

View File

@@ -582,7 +582,7 @@
<p><strong>2026-05-25 上游能力恢复版:</strong>用户明确要求“API 没关系,其他恢复,别削弱”。因此根域名画布恢复 <code>chatfire-AI/huobao-canvas</code> 的成熟节点和工作流结构推荐词、AI 润色、自动执行、工作流模板、首帧/尾帧/参考图节点、图片/视频/LLM 配置、多角度分镜、故事板、绘本和批量下载都保留;只继续替换品牌、路由和 API 接入。生成请求仍走 SKG 后端 <code>/api</code> 与登录 Cookie员工不需要个人 API Key。</p> <p><strong>2026-05-25 上游能力恢复版:</strong>用户明确要求“API 没关系,其他恢复,别削弱”。因此根域名画布恢复 <code>chatfire-AI/huobao-canvas</code> 的成熟节点和工作流结构推荐词、AI 润色、自动执行、工作流模板、首帧/尾帧/参考图节点、图片/视频/LLM 配置、多角度分镜、故事板、绘本和批量下载都保留;只继续替换品牌、路由和 API 接入。生成请求仍走 SKG 后端 <code>/api</code> 与登录 Cookie员工不需要个人 API Key。</p>
<p><strong>2026-05-25 媒体模型接入收口:</strong>图片和视频模型选择只暴露当前后端真实可用项:图片为 <code>auto</code><code>gpt-image-2</code><code>gemini-3-pro-image-preview</code>;视频当前只接通 <code>Seedance 2.0 Fast</code>(真实模型 <code>doubao-seedance-2-0-fast-260128</code>)。旧上游的 Nano Banana、Seedream、Kling、Veo 或浏览器本地自定义媒体模型不能进入生成下拉,避免同事选到实际不可用的模型。</p> <p><strong>2026-05-25 媒体模型接入收口:</strong>图片和视频模型选择只暴露当前后端真实可用项:图片为 <code>auto</code><code>gpt-image-2</code><code>gemini-3-pro-image-preview</code>;视频当前只接通 <code>Seedance 2.0 Fast</code>(真实模型 <code>doubao-seedance-2-0-fast-260128</code>)。旧上游的 Nano Banana、Seedream、Kling、Veo 或浏览器本地自定义媒体模型不能进入生成下拉,避免同事选到实际不可用的模型。</p>
<p><strong>2026-05-26 公司沉淀版:</strong>画布项目从浏览器本地存储升级为服务端 Postgres 持久化;<code>localStorage</code> 只作为离线缓存和首次导入来源。后端同时建立用户、任务、资源索引和审计表,保留原有 <code>state.json</code> 文件作为任务详情真源,避免一次迁移动到大文件资产结构。</p> <p><strong>2026-05-26 公司沉淀版:</strong>画布项目从浏览器本地存储升级为服务端 Postgres 持久化;<code>localStorage</code> 只作为离线缓存和首次导入来源。后端同时建立用户、任务、资源索引和审计表,保留原有 <code>state.json</code> 文件作为任务详情真源,避免一次迁移动到大文件资产结构。</p>
<p><strong>2026-05-26 AI 润色中性化:</strong>画布 <code>AI 润色</code> 不再复用 SKG 广告文案接口 <code>/creative/copy</code>。后端新增 <code>POST /prompt/polish</code>,前端 <code>useChat</code>、根画布输入框、文本节点和自动执行意图分析改走中性提示词/通用生成接口:只优化用户已经给出的主体、风格、镜头和细节,不主动添加 SKG、按摩产品、TikTok 广告话术或用户没有提到的品牌。</p> <p><strong>2026-05-26 AI 润色中性化:</strong>画布 <code>AI 润色</code> 不再复用 SKG 广告文案接口 <code>/creative/copy</code>。后端新增 <code>POST /prompt/polish</code>,前端 <code>useChat</code>、根画布输入框、文本节点和自动执行意图分析改走中性提示词/通用生成接口:只优化用户已经给出的主体、风格、镜头和细节,不主动添加 SKG、按摩产品、TikTok 广告话术或用户没有提到的品牌。润色会按人物意图分流:原提示词没有人物时明确保持物体/场景/产品构图且不新增人物;原提示词明确有人物、人像、模特或角色时,才补充“虚构 AI 角色、非真人、非公众人物”的合规描述。</p>
</div> </div>
<p>当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、AI 润色或工作流模板创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 <code>web/canvas-app/src/hooks/useApi.js</code> 适配到本项目 <code>/creative/jobs/image</code><code>/jobs/{id}/frames/{idx}/generate</code><code>/jobs/{id}/frames/{idx}/storyboard/video</code>AI 润色和通用 LLM 文本生成走 <code>/prompt/polish</code> 并保持中性专业,不再默认套入 SKG 广告语境。生成资产按当前登录用户写入个人 job。图片尺寸只显示 <code>auto</code><code>1024x1536</code><code>1024x1024</code><code>1536x1024</code>;视频画幅只显示 <code>720x1280</code><code>1280x720</code><code>1024x1024</code><code>960x1280</code>;视频时长只显示 <code>5/8/10/12/15</code> 秒。多人互不影响依赖后端 <code>owner_id</code>、画布项目 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。</p> <p>当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、AI 润色或工作流模板创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 <code>web/canvas-app/src/hooks/useApi.js</code> 适配到本项目 <code>/creative/jobs/image</code><code>/jobs/{id}/frames/{idx}/generate</code><code>/jobs/{id}/frames/{idx}/storyboard/video</code>AI 润色和通用 LLM 文本生成走 <code>/prompt/polish</code> 并保持中性专业,不再默认套入 SKG 广告语境。生成资产按当前登录用户写入个人 job。图片尺寸只显示 <code>auto</code><code>1024x1536</code><code>1024x1024</code><code>1536x1024</code>;视频画幅只显示 <code>720x1280</code><code>1280x720</code><code>1024x1024</code><code>960x1280</code>;视频时长只显示 <code>5/8/10/12/15</code> 秒。多人互不影响依赖后端 <code>owner_id</code>、画布项目 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。</p>
<div class="pipeline"> <div class="pipeline">
@@ -612,7 +612,7 @@
<tr><td><code>web/canvas-app/src/stores/projects.js</code></td><td>画布项目 Pinia store启动时先读本地 <code>localStorage["ai-canvas-projects"]</code> 作为缓存,再调用 <code>GET /canvas-projects</code> 拉服务端项目;如果发现本地旧项目,会调用 <code>POST /canvas-projects/import</code> 导入到当前登录用户。新建、重命名、画布节点变更、复制和删除会同步到 <code>/canvas-projects</code>,本地缓存只用于快速打开和网络异常兜底。</td></tr> <tr><td><code>web/canvas-app/src/stores/projects.js</code></td><td>画布项目 Pinia store启动时先读本地 <code>localStorage["ai-canvas-projects"]</code> 作为缓存,再调用 <code>GET /canvas-projects</code> 拉服务端项目;如果发现本地旧项目,会调用 <code>POST /canvas-projects/import</code> 导入到当前登录用户。新建、重命名、画布节点变更、复制和删除会同步到 <code>/canvas-projects</code>,本地缓存只用于快速打开和网络异常兜底。</td></tr>
<tr><td><code>web/canvas-app/src/views/Canvas.vue</code></td><td>画布主交互:恢复上游底部 prompt composer、<code>AI 润色</code><code>自动执行</code>、推荐词、节点菜单、工作流面板、API/模型设置入口和批量下载入口。自动执行会调用 <code>useWorkflowOrchestrator</code> 分析提示词,创建文生图、图转视频、故事板、多角度分镜或绘本节点组;手动模式只创建文本节点,用户自行连接节点。</td></tr> <tr><td><code>web/canvas-app/src/views/Canvas.vue</code></td><td>画布主交互:恢复上游底部 prompt composer、<code>AI 润色</code><code>自动执行</code>、推荐词、节点菜单、工作流面板、API/模型设置入口和批量下载入口。自动执行会调用 <code>useWorkflowOrchestrator</code> 分析提示词,创建文生图、图转视频、故事板、多角度分镜或绘本节点组;手动模式只创建文本节点,用户自行连接节点。</td></tr>
<tr><td><code>web/canvas-app/src/config/models.js</code></td><td>画布媒体模型和规格的前端白名单:图片只内置 <code>auto</code><code>gpt-image-2</code><code>gemini-3-pro-image-preview</code>,尺寸只内置 <code>auto</code><code>1024x1536</code><code>1024x1024</code><code>1536x1024</code>;视频只内置 <code>seedance</code> / <code>Seedance 2.0 Fast</code>,画幅和时长对齐后端 <code>/health</code> 能力边界。<code>useModelConfig.js</code> 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,防止旧缓存把不可用模型带回生成下拉。</td></tr> <tr><td><code>web/canvas-app/src/config/models.js</code></td><td>画布媒体模型和规格的前端白名单:图片只内置 <code>auto</code><code>gpt-image-2</code><code>gemini-3-pro-image-preview</code>,尺寸只内置 <code>auto</code><code>1024x1536</code><code>1024x1024</code><code>1536x1024</code>;视频只内置 <code>seedance</code> / <code>Seedance 2.0 Fast</code>,画幅和时长对齐后端 <code>/health</code> 能力边界。<code>useModelConfig.js</code> 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,防止旧缓存把不可用模型带回生成下拉。</td></tr>
<tr><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布到本项目后端的适配层:不再读取浏览器 API Key而是使用当前登录会话 Cookie 调用 <code>/api</code>。文生图 / 图生图先创建轻量 creative job再调用 <code>/frames/0/generate</code>;文生视频 / 图生视频调用 <code>/storyboard/video</code> 并轮询 <code>/jobs/{id}</code>,完成后把图片或 mp4 URL 写回画布节点。<code>useChat</code> 已从 SKG 广告文案接口切到 <code>/prompt/polish</code>AI 润色显式使用 image/video prompt 模式LLM 节点使用通用 chat 模式,避免自动注入用户没有提到的 SKG 或营销语境。</td></tr> <tr><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布到本项目后端的适配层:不再读取浏览器 API Key而是使用当前登录会话 Cookie 调用 <code>/api</code>。文生图 / 图生图先创建轻量 creative job再调用 <code>/frames/0/generate</code>;文生视频 / 图生视频调用 <code>/storyboard/video</code> 并轮询 <code>/jobs/{id}</code>,完成后把图片或 mp4 URL 写回画布节点。<code>useChat</code> 已从 SKG 广告文案接口切到 <code>/prompt/polish</code>AI 润色显式使用 image/video prompt 模式LLM 节点使用通用 chat 模式,避免自动注入用户没有提到的 SKG 或营销语境;后端会判断原提示词是否有人物意图,无人物时禁止新增人物,有人物时才声明虚构 AI 角色</td></tr>
<tr><td><code>web/scripts/sync-canvas-root.mjs</code></td><td>构建桥接脚本:在 <code>next build</code> 静态导出完成后,把 Vite 画布产物 <code>web/canvas-app/dist</code> 覆盖到 <code>web/out</code> 根目录,使 <code>https://marketing.skg.com</code> 登录后直接进入画布;旧 <code>web/scripts/sync-canvas-dist.mjs</code> 保留但不再由生产构建调用。</td></tr> <tr><td><code>web/scripts/sync-canvas-root.mjs</code></td><td>构建桥接脚本:在 <code>next build</code> 静态导出完成后,把 Vite 画布产物 <code>web/canvas-app/dist</code> 覆盖到 <code>web/out</code> 根目录,使 <code>https://marketing.skg.com</code> 登录后直接进入画布;旧 <code>web/scripts/sync-canvas-dist.mjs</code> 保留但不再由生产构建调用。</td></tr>
<tr><td><code>web/app/detail/page.tsx</code></td><td>任务详情页:静态导出路由 <code>/detail/?job=&lt;id&gt;</code>,通过 query 读取 job id调用 <code>getJob</code> 恢复同一任务。页面展示参考图、全部生成图、视频候选、营销图文方案和历史提示词,可继续调用 <code>generateImage</code><code>generateStoryboardVideo</code><code>generateCreativeCopy</code>,并支持删除图片/视频。该页继续依赖后端 owner 过滤,用户不能通过切换 URL 读取别人的任务。</td></tr> <tr><td><code>web/app/detail/page.tsx</code></td><td>任务详情页:静态导出路由 <code>/detail/?job=&lt;id&gt;</code>,通过 query 读取 job id调用 <code>getJob</code> 恢复同一任务。页面展示参考图、全部生成图、视频候选、营销图文方案和历史提示词,可继续调用 <code>generateImage</code><code>generateStoryboardVideo</code><code>generateCreativeCopy</code>,并支持删除图片/视频。该页继续依赖后端 owner 过滤,用户不能通过切换 URL 读取别人的任务。</td></tr>
<tr><td><code>web/app/agent/page.tsx</code></td><td>新增一键出片终端页:只保留 TikTok 链接、产品图上传、实时 <code>Agent Terminal</code> 和最终成片播放器;通过 <code>POST /agent-runs</code> 创建受限后台状态机任务,通过 <code>GET /agent-runs/{id}</code> 轮询日志、进度、审片图和最终 mp4。该页不替代旧工作台深度编辑能力只承接“用户只看成品”的快速出片主路径。</td></tr> <tr><td><code>web/app/agent/page.tsx</code></td><td>新增一键出片终端页:只保留 TikTok 链接、产品图上传、实时 <code>Agent Terminal</code> 和最终成片播放器;通过 <code>POST /agent-runs</code> 创建受限后台状态机任务,通过 <code>GET /agent-runs/{id}</code> 轮询日志、进度、审片图和最终 mp4。该页不替代旧工作台深度编辑能力只承接“用户只看成品”的快速出片主路径。</td></tr>
@@ -643,7 +643,7 @@
<h3>后端核心</h3> <h3>后端核心</h3>
<table> <table>
<tbody> <tbody>
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回同时承载全局 <code>prompt_library</code><code>asset_library</code> 的磁盘索引、CRUD、删除保护和复制到 job API。启动时会初始化 Postgres schema、扫描现有 <code>state.json</code> / 资源库并写入索引;新增 <code>/canvas-projects</code> 系列接口把画布项目按当前登录用户持久化。轻量创作入口 <code>POST /creative/jobs/image</code> 把上传图片或空白底图写成一个只有 0 号关键帧的 <code>Job</code>,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;<code>POST /prompt/polish</code> 用于中性 AI 润色和通用 LLM 文本生成,只保留用户明确给出的主体、品牌、产品、地点、风格和意图,不默认加入 SKG、按摩产品或短视频广告话术<code>/health</code> 返回 <code>database</code><code>image_options</code><code>image_size_options</code><code>video_options</code><code>video_size_options</code><code>video_duration_options</code><code>video_max_duration_seconds</code><code>/frames/{idx}/generate</code><code>model</code> 字段用于图片模型偏好,<code>size</code> 字段用于图片输出尺寸;<code>/storyboard/video</code> 继续使用 <code>model</code> 字段选择视频别名,并先校验画幅与时长能力边界,然后把 <code>GeneratedVideo</code> 写成 <code>queued</code> 占位并进入进程内视频队列。队列默认 <code>VIDEO_QUEUE_MAX_CONCURRENT=2</code><code>VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1</code>,同一用户连续提交不会占满全局并发;排队任务会回写 <code>queue_position</code><code>queue_size</code><code>queue_message</code>。旧 <code>AgentRun</code> 一键出片状态机、TK 复刻接口和 <code>POST /creative/copy</code> 作为明确的 SKG 营销文案接口继续保留。</td></tr> <tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回同时承载全局 <code>prompt_library</code><code>asset_library</code> 的磁盘索引、CRUD、删除保护和复制到 job API。启动时会初始化 Postgres schema、扫描现有 <code>state.json</code> / 资源库并写入索引;新增 <code>/canvas-projects</code> 系列接口把画布项目按当前登录用户持久化。轻量创作入口 <code>POST /creative/jobs/image</code> 把上传图片或空白底图写成一个只有 0 号关键帧的 <code>Job</code>,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;<code>POST /prompt/polish</code> 用于中性 AI 润色和通用 LLM 文本生成,只保留用户明确给出的主体、品牌、产品、地点、风格和意图,不默认加入 SKG、按摩产品或短视频广告话术,并通过 <code>_prompt_has_person_intent</code> / <code>_prompt_person_guard</code> 区分“无人画面不新增人物”和“有人物画面声明虚构 AI 角色”<code>/health</code> 返回 <code>database</code><code>image_options</code><code>image_size_options</code><code>video_options</code><code>video_size_options</code><code>video_duration_options</code><code>video_max_duration_seconds</code><code>/frames/{idx}/generate</code><code>model</code> 字段用于图片模型偏好,<code>size</code> 字段用于图片输出尺寸;<code>/storyboard/video</code> 继续使用 <code>model</code> 字段选择视频别名,并先校验画幅与时长能力边界,然后把 <code>GeneratedVideo</code> 写成 <code>queued</code> 占位并进入进程内视频队列。队列默认 <code>VIDEO_QUEUE_MAX_CONCURRENT=2</code><code>VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1</code>,同一用户连续提交不会占满全局并发;排队任务会回写 <code>queue_position</code><code>queue_size</code><code>queue_message</code>。旧 <code>AgentRun</code> 一键出片状态机、TK 复刻接口和 <code>POST /creative/copy</code> 作为明确的 SKG 营销文案接口继续保留。</td></tr>
<tr><td><code>api/db.py</code></td><td>Postgres 适配层:在 <code>DATABASE_URL</code> 存在且 <code>psycopg</code> 可用时启用;负责建表、健康检查、用户 upsert、审计日志、画布项目 CRUD以及把 <code>Job</code><code>AgentRun</code>、提示词库和素材库写入索引表。数据库不可用时本地开发会降级为 disabled生产 <code>verify-prod-docker.sh</code> 会要求 <code>database.connected=true</code></td></tr> <tr><td><code>api/db.py</code></td><td>Postgres 适配层:在 <code>DATABASE_URL</code> 存在且 <code>psycopg</code> 可用时启用;负责建表、健康检查、用户 upsert、审计日志、画布项目 CRUD以及把 <code>Job</code><code>AgentRun</code>、提示词库和素材库写入索引表。数据库不可用时本地开发会降级为 disabled生产 <code>verify-prod-docker.sh</code> 会要求 <code>database.connected=true</code></td></tr>
<tr><td><code>video_model_options()</code></td><td>视频模型能力出口:如果 <code>seedance</code><code>kling</code><code>veo3</code><code>veo</code> 等业务别名实际都映射到同一个真实模型,会按真实模型去重,只给前端返回一个可用选项;当前生产真实模型为 <code>doubao-seedance-2-0-fast-260128</code>,前端显示为 <code>Seedance 2.0 Fast</code>。后续只有在服务器真的配置了不同可用视频模型时,才应把新的模型重新暴露给画布。</td></tr> <tr><td><code>video_model_options()</code></td><td>视频模型能力出口:如果 <code>seedance</code><code>kling</code><code>veo3</code><code>veo</code> 等业务别名实际都映射到同一个真实模型,会按真实模型去重,只给前端返回一个可用选项;当前生产真实模型为 <code>doubao-seedance-2-0-fast-260128</code>,前端显示为 <code>Seedance 2.0 Fast</code>。后续只有在服务器真的配置了不同可用视频模型时,才应把新的模型重新暴露给画布。</td></tr>
<tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr> <tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr>
@@ -1085,7 +1085,7 @@ ProductRefStateItem {
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;后端会把当前登录用户写入 <code>Job.owner_*</code>,后续详情、素材文件、删除和生成接口都通过统一中间件校验归属。下载阶段默认不带 cookies生产环境必须显式保持 <code>YTDLP_COOKIES_FILE=</code><code>YTDLP_COOKIES_FROM_BROWSER=</code> 为空,避免容器内误读被打进镜像的开发 <code>api/.env</code></td></tr> <tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;后端会把当前登录用户写入 <code>Job.owner_*</code>,后续详情、素材文件、删除和生成接口都通过统一中间件校验归属。下载阶段默认不带 cookies生产环境必须显式保持 <code>YTDLP_COOKIES_FILE=</code><code>YTDLP_COOKIES_FROM_BROWSER=</code> 为空,避免容器内误读被打进镜像的开发 <code>api/.env</code></td></tr>
<tr><td>画布项目</td><td><code>GET /canvas-projects</code><br><code>POST /canvas-projects</code><br><code>PUT /canvas-projects/{id}</code><br><code>GET /canvas-projects/{id}</code><br><code>DELETE /canvas-projects/{id}</code><br><code>POST /canvas-projects/import</code></td><td><code>web/canvas-app/src/stores/projects.js</code></td><td>根域名画布项目的服务端持久化接口。列表和详情按当前登录用户过滤;写入时保存画布 JSON、缩略图、可见性、版本和更新时间删除为软删除。首次上线后本地 <code>localStorage</code> 旧项目会通过 import 导入到当前用户,之后服务端 Postgres 是主存储。</td></tr> <tr><td>画布项目</td><td><code>GET /canvas-projects</code><br><code>POST /canvas-projects</code><br><code>PUT /canvas-projects/{id}</code><br><code>GET /canvas-projects/{id}</code><br><code>DELETE /canvas-projects/{id}</code><br><code>POST /canvas-projects/import</code></td><td><code>web/canvas-app/src/stores/projects.js</code></td><td>根域名画布项目的服务端持久化接口。列表和详情按当前登录用户过滤;写入时保存画布 JSON、缩略图、可见性、版本和更新时间删除为软删除。首次上线后本地 <code>localStorage</code> 旧项目会通过 import 导入到当前用户,之后服务端 Postgres 是主存储。</td></tr>
<tr><td>画布生成</td><td><code>POST /creative/jobs/image</code><br><code>POST /jobs/{id}/frames/upload</code><br><code>POST /jobs/{id}/frames/{idx}/generate</code><br><code>POST /jobs/{id}/frames/{idx}/storyboard/video</code><br><code>GET /jobs/{id}</code></td><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布项目结构保存在 <code>/canvas-projects</code>;一旦生成图片或视频,就通过同一套 creative job / frame / storyboard video 接口写入当前登录用户自己的 job 目录。文生图会创建空白 creative job 后生成图片;图生视频会把上传图转成 frame 并作为视频参考图提交,提交视频后用 <code>skg:{jobId}:{videoId}</code> 作为画布侧任务 id 轮询 <code>/jobs/{id}</code>,直到视频状态完成或失败。</td></tr> <tr><td>画布生成</td><td><code>POST /creative/jobs/image</code><br><code>POST /jobs/{id}/frames/upload</code><br><code>POST /jobs/{id}/frames/{idx}/generate</code><br><code>POST /jobs/{id}/frames/{idx}/storyboard/video</code><br><code>GET /jobs/{id}</code></td><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布项目结构保存在 <code>/canvas-projects</code>;一旦生成图片或视频,就通过同一套 creative job / frame / storyboard video 接口写入当前登录用户自己的 job 目录。文生图会创建空白 creative job 后生成图片;图生视频会把上传图转成 frame 并作为视频参考图提交,提交视频后用 <code>skg:{jobId}:{videoId}</code> 作为画布侧任务 id 轮询 <code>/jobs/{id}</code>,直到视频状态完成或失败。</td></tr>
<tr><td>AI 润色 / LLM 节点</td><td><code>POST /prompt/polish</code></td><td><code>web/canvas-app/src/hooks/useApi.js</code><br><code>web/canvas-app/src/api/chat.js</code></td><td>中性的提示词润色和通用文本生成接口。根画布和文本节点传 <code>mode=image</code>、默认输出英文提示词LLM 节点和自动执行意图分析传 <code>mode=chat</code>、保持输入语言。接口会遵守 system prompt但明确禁止自动添加用户没有提到的 SKG、按摩产品、短视频广告 framing、营销标题或 hashtag。</td></tr> <tr><td>AI 润色 / LLM 节点</td><td><code>POST /prompt/polish</code></td><td><code>web/canvas-app/src/hooks/useApi.js</code><br><code>web/canvas-app/src/api/chat.js</code></td><td>中性的提示词润色和通用文本生成接口。根画布和文本节点传 <code>mode=image</code>、默认输出英文提示词LLM 节点和自动执行意图分析传 <code>mode=chat</code>、保持输入语言。接口会遵守 system prompt但明确禁止自动添加用户没有提到的 SKG、按摩产品、短视频广告 framing、营销标题或 hashtag。人物安全词按输入条件加入:原文无人物语义时追加“不新增人物/脸/身体/人群”;原文有人像、模特、角色、数字人等语义时才追加“虚构 AI 角色、非真人、非公众人物、不可识别私人个体”。</td></tr>
<tr><td>一键出片终端</td><td><code>POST /agent-runs</code><br><code>GET /agent-runs</code><br><code>GET /agent-runs/{id}</code><br><code>GET /agent-runs/{id}/final.mp4</code><br><code>GET /agent-runs/{id}/contact.jpg</code></td><td><code>web/app/agent/page.tsx</code></td><td>快速出片页的唯一主接口。前端提交 TikTok 链接和最多 6 张产品图;后端创建同 owner 的 <code>Job</code><code>AgentRun</code>后台执行下载、产品图归一化、透明骨架主体参考复制、12 段镜头计划、视频生成、失败镜头自动重跑一次、审片接触表和 ffmpeg 最终合成。列表、详情、最终 mp4 和接触表同样按 owner 隔离。</td></tr> <tr><td>一键出片终端</td><td><code>POST /agent-runs</code><br><code>GET /agent-runs</code><br><code>GET /agent-runs/{id}</code><br><code>GET /agent-runs/{id}/final.mp4</code><br><code>GET /agent-runs/{id}/contact.jpg</code></td><td><code>web/app/agent/page.tsx</code></td><td>快速出片页的唯一主接口。前端提交 TikTok 链接和最多 6 张产品图;后端创建同 owner 的 <code>Job</code><code>AgentRun</code>后台执行下载、产品图归一化、透明骨架主体参考复制、12 段镜头计划、视频生成、失败镜头自动重跑一次、审片接触表和 ffmpeg 最终合成。列表、详情、最终 mp4 和接触表同样按 owner 隔离。</td></tr>
<tr><td>重试下载</td><td><code>POST /jobs/{id}/download/retry</code></td><td><code>retryJobDownload</code></td><td>用于 TK 链接下载失败且没有 <code>video_url</code> 的素材;清空错误、重新进入下载状态,并在后台再次执行 <code>pipeline_download</code>。上传视频不能重下载,需要重新上传文件。</td></tr> <tr><td>重试下载</td><td><code>POST /jobs/{id}/download/retry</code></td><td><code>retryJobDownload</code></td><td>用于 TK 链接下载失败且没有 <code>video_url</code> 的素材;清空错误、重新进入下载状态,并在后台再次执行 <code>pipeline_download</code>。上传视频不能重下载,需要重新上传文件。</td></tr>
<tr><td>上传视频</td><td><code>POST /jobs/upload</code></td><td><code>uploadJob</code></td><td>保存 source.mp4然后同样进入下载完成状态当前上传后也加入第一步队列下载完成后自动解析音频。</td></tr> <tr><td>上传视频</td><td><code>POST /jobs/upload</code></td><td><code>uploadJob</code></td><td>保存 source.mp4然后同样进入下载完成状态当前上传后也加入第一步队列下载完成后自动解析音频。</td></tr>
@@ -1243,6 +1243,18 @@ ProductRefStateItem {
<h2>变更记录</h2> <h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p> <p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog"> <div class="changelog">
<article class="change">
<header>
<h3>2026-05-26 · AI 润色按人物意图加安全词</h3>
<span class="tag amber">API</span>
<span class="tag violet">Canvas</span>
</header>
<div class="body">
<p><strong>问题:</strong>为了降低 Seedance 人脸/肖像风控,不能把“虚构 AI 角色、非真人”无条件加到所有润色结果里;无人画面如果出现 <code>character</code><code>avatar</code> 等词,反而可能诱导模型新增人物。</p>
<p><strong>改动:</strong><code>api/main.py</code> 新增人物意图检测原始输入含人像、模特、角色、数字人、脸、portrait、model、character 等语义时润色才补“fully fictional synthetic AI character / virtual avatar / not based on any real person”原始输入没有人物语义或明确写无人时润色改为补“保持 object-only / scene-only / product-only composition不新增 people、faces、bodies、hands、avatars、characters、crowds”。</p>
<p><strong>影响:</strong>无人物产品图、场景图、食物图、街景图不会因为合规词被润成有人画面;有人物需求仍会尽量声明为虚构 AI 角色,减少被上游误判为真实肖像的概率。参考图里已有清晰人脸时,最终仍由上游图片安全检测决定,失败后继续显示可读的人脸风控提示。</p>
</div>
</article>
<article class="change"> <article class="change">
<header> <header>
<h3>2026-05-26 · AI 润色从 SKG 广告文案拆出</h3> <h3>2026-05-26 · AI 润色从 SKG 广告文案拆出</h3>