Skip to content

feat(prompt): 用户描述先过措辞门禁与适配器 - #321

Open
johnnyzhang-eng wants to merge 1 commit into
1024XEngineer:mainfrom
johnnyzhang-eng:feat/prompt-adapter-v2
Open

feat(prompt): 用户描述先过措辞门禁与适配器#321
johnnyzhang-eng wants to merge 1 commit into
1024XEngineer:mainfrom
johnnyzhang-eng:feat/prompt-adapter-v2

Conversation

@johnnyzhang-eng

Copy link
Copy Markdown
Contributor

Refs #309

改了什么

用户那句动作描述以前是原样进提示词骨架的,现在先过一道措辞门禁 + 一层零模型的适配器,拦在付费调用之前。

  • prompt/lint.py:纯函数措辞门禁,只依赖标准库。八类规则,每类对应一条模型侧机制(否定式 / 特效名词 / 装备形状先验 / 冲击词 / 亚阈值微动 / 持物无身体锚 / 装备名词),报错按机制写而不是"命中禁词"——收到它的人要据此改自己的描述。
  • prompt/adapter.py:规则版适配器 RuleBasedPromptAdapter,零模型、零成本、确定性。跑门禁、error 级当场拒、把那句话嵌进已验证的骨架、追加单主体与构图后缀。
  • ports/__init__.py:AdaptedPrompt / PromptAdapterPort / PromptRejected / PromptRejectCode。拒绝理由带 code,server 据此选文案,不用去 parse 异常消息。
  • models/character.py:CharacterStance 枚举 + CharacterCard.stance,默认 biped
  • prompt/custom.py:抽出 build_custom_body(不含循环性尾句),两条尾句转公开。
  • strategy/concrete.py:CUSTOM 分支接适配器。

为什么

#309 的根因是动作描述在一条路径上有适配、在另一条路径上直传,而两条路径对入参的语义要求不同。这个 PR 先把"适配"这件事做成一个有协议、可测、可复用的组件,并在视频路线的 custom 分支上接通。

几条判定的机制:

  • 否定式:这条通路没有 negative_prompt,模型不处理否定极性、只把名词 latch 进画面,"不要 X" 等于点名要 X。所以本 PR 走的是加正向约束(单主体 + 整身入画 + 纯色背景)而不是补负向词。
  • 多阶段:静态模型没有时间轴,"蓄力后攻击再收势" 会被摊平成并排的分解姿势图——就是 攻击生成出现多个人物:动作描述被原样当成图像描述 #309 那张双人图。故 kind="still" 时多阶段标记直接拒,并强制追加"单一冻结瞬间"。同一句话对 kind="i2v" 是合法的,不能一刀切。
  • 体型:非双足角色的描述里出现"手臂",模型会给它凭空接上一对人的上肢来调和文图矛盾,而帧数、时长、成色全部正常,只在画面上显形。所以 stance 参与判定;每个非双足成员在适配器里都有自己那套部位说法,否则拒绝理由只能说"不行"、给不出改法。
  • 亚阈值微动对 i2v 是 error(拿回的是逐帧随机抖动),对 still 只是 warn。

两条工程约束:

  • except PromptRejected: raise 排在宽泛的 except 之前。它是 ValueError 子类,放后面永远轮不上,那样一段必然出坏产物的描述会被"回退到旧骨架"接住,照样把钱花掉。
  • 适配器本身坏掉(非 PromptRejected 的任何异常)只丢掉那层改写,回退到原有 build_custom_prompt,不打死整条生成线。
  • 适配器不产出循环性尾句:循环与否是请求的属性(ActionSpec.cyclic),适配器入参里没有,替调用方猜一条会把一次性动作首尾闭环,而帧数时长全对、看不出来。

怎么验证的

backend/tests/test_prompt_adapter.py,30 个用例,全部离线零成本:

  • 仓库里已发布的提示词文档自己过一遍门禁(其中攻击提示词被形状先验规则逮到)
  • 每个 PromptRejectCode 都可达、都带机制说明;两条规则同时命中时两条都列出来
  • 端到端(打桩 provider,不调真模型):被拒的描述没有到达付费调用;适配器抛通用异常时回退到旧骨架;固定动作分支不经过适配器;CharacterCard.stance 确实传到了适配器;四足角色写"手臂"端到端被拒、双足同一句放行

本地 backend/ 下:

  • uv run ruff check . → All checks passed
  • uv run lint-imports → 2 kept, 0 broken
  • uv run pytest -q → 731 passed, 14 skipped

范围说明

只含后端。ports/__init__.pymodels/character.pystrategy/concrete.py 这几个文件同期还有别的改动(成色台账字段、攻击原型分支),那些不在本 PR 里。

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 14, 2026 9:52am

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论

这次改动把 custom 描述门禁放到了付费调用前,异常回退与循环尾句的契约也保持一致。不过,生产入口尚未传递体型,结构化拒绝码也没有穿过异步任务边界;另外两类常见中文措辞仍会绕过新门禁。

验证包括固定 SHA 全量 diff 审阅、调用链静态追踪、纯标准库 lint 复现和 compileall。当前环境缺少 uv,未重复运行 PR 中声明的 pytest/ruff/import-linter。

View job run

# 不往提示词里加任何部位词、也不要求用户改写措辞。反过来把默认设成非双足,会让占多数的
# 人形角色被要求把"手臂"改写成"前肢 / 尾",那些词进提示词就是让模型凭空长出对应部位。
# 不自动识别:那要调模型,而认错是静默的 —— 错误的体型只在下一次付费生成的画面上显形。
stance: CharacterStance = CharacterStance.BIPED

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 将角色体型从生产请求传入 CharacterCard

当前唯一的生产构造点 ActionTaskExecutor._produce_action 仍只传 name/desc,而 API、CharacterActionInput 和持久化的 CharacterData 也没有 stance 来源,因此所有真实请求都会落到这里的 BIPED 默认值。结果是四足/蛇形角色在生产链路中永远不会触发本 PR 新增的 STANCE_MISMATCH,像“raises the left arm”仍会进入付费调用并生成错误肢体。请把显式体型字段贯穿请求/角色数据到 CharacterCard,或在启用该规则前提供另一个可靠来源。

_NEGATION = (
"no", "not", "never", "without", "avoid", "avoids", "avoiding",
"don't", "doesn't", "isn't", "aren't", "won't", "cannot",
"不要", "不能", "不许", "禁止", "避免", "勿",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 覆盖常见中文和英文否定式

否定门禁只列了少数完整短语,常见输入如 向前走,不转身can't turn around 当前都会得到空 issue 并继续进入模型;这正是本规则要阻止的“否定极性丢失后名词/动作被 latch”路径。请补齐常见否定形式,尤其是中文 不+动词 与英文 can't/弯引号 contraction,并添加这些实际句式的回归用例。

"then", "after that", "afterwards", "followed by", "next,", "and finally",
"然后", "接着", "紧接着", "之后", "再", "最后", "先", "收势",
)
_ARM_WORDS = ("arm", "arms", "elbow", "hand", "hands", "手臂", "胳膊", "手肘")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 识别中文“手”类体型冲突措辞

非双足门禁覆盖了 手臂/胳膊/手肘,却漏掉更常见的 手/左手/双手;例如四足角色描述 举起左手 不会命中 _ARM_WORDS,会直接进入付费 i2v,而英文等价句会被拒绝。请用能覆盖这些中文组合的匹配规则,并避免简单加入单字后对无关词造成误报。


def __init__(self, code: PromptRejectCode, detail: str) -> None:
super().__init__(f"这段描述跑不出可用产物({code.value}):{detail}")
self.code = code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 让 PromptRejectCode 穿过异步任务边界

这里虽然保存了结构化 code,但生产调用方 run_action_task 用宽泛的 except Exception 捕获后只持久化 str(exc)GenerationTaskOut 也只有 error_message。因此 server 实际无法按 code 选文案或区分可修改的 4xx 输入错误与引擎 5xx,除非重新解析异常字符串,正好违背这个端口的设计目的。请在任务失败结果中单独序列化拒绝码(或专门捕获 PromptRejected 并完成映射)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant