-
Notifications
You must be signed in to change notification settings - Fork 4
feat(judge): 增加只判可数四问的判官与 shadow 闸口 #324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
johnnyzhang-eng
wants to merge
1
commit into
1024XEngineer:main
Choose a base branch
from
johnnyzhang-eng:feat/quality-judge-v2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+907
−25
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
backend/packages/app/src/windup_app/server/orchestrator/quality_gate.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| """出门那道闸:把判官读数翻成"交不交付"。 | ||
|
|
||
| 在 server 而不是 ai_engine,因为判出问题之后的每一种处置(退款、重跑、换母版)都要 | ||
| 再花一次钱,那是产品决策;引擎的分工是如实报数,判决归调用方。 | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from dataclasses import dataclass | ||
|
|
||
| from windup_ai_engine.ports import JudgePort, JudgeVerdict | ||
| from windup_framework.config.quality_gate import QualityGateSettings, settings | ||
|
|
||
| logger = logging.getLogger("windup.generation.quality_gate") | ||
|
|
||
| # 稳定的机器可读标签。用槽位名而不是句子,是因为下游要按它分桶统计; | ||
| # 给人看的解释在 :func:`_problems` 的判据里,不在这几个字符串里。 | ||
| PROBLEM_MULTIPLE_SUBJECTS = "multiple_subjects" | ||
| PROBLEM_NO_SUBJECT = "no_subject" | ||
| PROBLEM_FOREIGN_OBJECTS = "foreign_objects" | ||
| PROBLEM_ACTION_MISMATCH = "action_mismatch" | ||
| PROBLEM_CLIPPED = "clipped" | ||
|
|
||
| _EXPECTED_SUBJECTS = 1 | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class GateDecision: | ||
| """一次判读的结论。 | ||
|
|
||
| ``verdict`` 有值 = 判过了,``problems`` 可能为空;``error`` 有值 = 判官坏了、 | ||
| 什么都没判出来,那不是"通过"。压成一个布尔的话这两种状态就没法分开了。 | ||
| """ | ||
|
|
||
| frame_index: int | ||
| problems: tuple[str, ...] = () | ||
| blocked: bool = False | ||
| verdict: JudgeVerdict | None = None | ||
| error: str | None = None | ||
|
|
||
| def as_payload(self) -> dict: | ||
| """写进任务结果的形状 —— 复核要用的东西一样不少(含模型原话)。""" | ||
| data: dict = { | ||
| "frame_index": self.frame_index, | ||
| "problems": list(self.problems), | ||
| "blocked": self.blocked, | ||
| } | ||
| if self.error is not None: | ||
| data["error"] = self.error | ||
| if self.verdict is not None: | ||
| data["subject_count"] = self.verdict.subject_count | ||
| data["foreign_objects"] = list(self.verdict.foreign_objects) | ||
| data["action_matches"] = self.verdict.action_matches | ||
| data["clipped"] = self.verdict.clipped | ||
| data["raw"] = self.verdict.raw | ||
| return data | ||
|
|
||
|
|
||
| class QualityBlocked(ValueError): | ||
| """判官判出问题且闸口处于拦截档 —— 不交付。""" | ||
|
|
||
| def __init__(self, problems: tuple[str, ...]) -> None: | ||
| super().__init__(f"交付被判官拦下:{', '.join(problems)}") | ||
| self.problems = problems | ||
|
|
||
|
|
||
| def pick_frame(count: int) -> int: | ||
| """只判一帧 —— 判满一段等于把成本乘上帧数。 | ||
|
|
||
| 取中间那帧而不是首帧:首帧最接近母版,正是"动作对不对"最看不出来的一帧。 | ||
| """ | ||
| return count // 2 | ||
|
|
||
|
|
||
| def review( | ||
| judge: JudgePort | None, | ||
| frames: list[bytes], | ||
| master: bytes, | ||
| action: str, | ||
| *, | ||
| config: QualityGateSettings = settings, | ||
| ) -> GateDecision | None: | ||
| """判一段交付物;``None`` = 没判(没注入判官或闸口未启用)。 | ||
|
|
||
| "没判"要与"判了没问题"分得开:只有后者能支持"这批产物是干净的"这句话。 | ||
| """ | ||
| if judge is None or not config.enabled or not frames: | ||
| return None | ||
|
|
||
| index = pick_frame(len(frames)) | ||
| try: | ||
| verdict = judge.judge(frames[index], master, action) | ||
| except Exception as exc: # noqa: BLE001 —— 判官的任何故障都归"仪器坏了" | ||
| # 仪器故障绝不拦截:拦下去等于因为我们自己的判官挂了,把用户已付费的产物扣住。 | ||
| logger.warning("判官判读失败(第 %d 帧,动作 %s):%s", index, action, exc) | ||
| return GateDecision(frame_index=index, error=str(exc)) | ||
|
|
||
| problems = _problems(verdict) | ||
| # shadow 档(``enforce`` 默认 false)只记不拦:阈值要拿 shadow 数据定,反过来先开 | ||
| # 拦截就是拍脑袋定判据,而误杀掉的是用户已付费、退不回来的产物。 | ||
| blocked = bool(problems) and config.enforce | ||
| if problems: | ||
| logger.info("判官在第 %d 帧读出 %s(拦截=%s)", index, problems, blocked) | ||
| return GateDecision( | ||
| frame_index=index, problems=problems, blocked=blocked, verdict=verdict, | ||
| ) | ||
|
|
||
|
|
||
| def _problems(verdict: JudgeVerdict) -> tuple[str, ...]: | ||
| """四问 → 问题标签。每一条都有唯一答案,不含任何阈值。""" | ||
| found: list[str] = [] | ||
| if verdict.subject_count > _EXPECTED_SUBJECTS: | ||
| found.append(PROBLEM_MULTIPLE_SUBJECTS) | ||
| elif verdict.subject_count < _EXPECTED_SUBJECTS: | ||
| found.append(PROBLEM_NO_SUBJECT) | ||
| if verdict.foreign_objects: | ||
| found.append(PROBLEM_FOREIGN_OBJECTS) | ||
| if not verdict.action_matches: | ||
| found.append(PROBLEM_ACTION_MISMATCH) | ||
| if verdict.clipped: | ||
| found.append(PROBLEM_CLIPPED) | ||
| return tuple(found) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
backend/packages/common/src/windup_common/models/quality.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| """判官读数的数据契约。 | ||
|
|
||
| 落在 common 而不是 ai_engine.ports,因为构造它的是 framework 层的 provider,而分层 | ||
| 门禁禁止 framework 依赖 ai_engine;调用方仍从 ``windup_ai_engine.ports`` 取。 | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
|
|
||
| # 刻意**没有** score 字段:主观评分的噪声大到能盖过真实差异,而出参里一旦有个分数,迟早 | ||
| # 有人拿它卡阈值 —— 那时卡掉的是噪声,每一次误杀都是用户已付费、不可退的产物。下面四项 | ||
| # 各有唯一答案,人眼复核一遍就能确认对错。"好看"由输入端(母版规格 + 提示词骨架)保证。 | ||
| @dataclass(frozen=True) | ||
| class JudgeVerdict: | ||
| """一帧交付物的四个可数、可复核读数 —— 不含"好不好看"的判断。""" | ||
|
|
||
| subject_count: int | ||
| """画面里出现了几个角色主体。期望 1;≥2 通常是 i2v 把角色分裂成了两个。""" | ||
|
|
||
| foreign_objects: tuple[str, ...] | ||
| """母版里没有、生成帧里却出现的物体名。空元组 = 没有多出来的东西。""" | ||
|
|
||
| action_matches: bool | ||
| """这一帧的姿态是否属于所要求的动作类别 —— 判类别,不判动作做得好不好。""" | ||
|
|
||
| clipped: bool | ||
| """角色是否被画面边缘裁到。""" | ||
|
|
||
| raw: str | ||
| """模型原话。留着是为了让人能复核判读本身对不对 —— 四个读数都是模型给的, | ||
| 判官自己出错时,没有原话就无从分辨"产物真有问题"和"判官读错了"。""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
backend/packages/framework/src/windup_framework/config/quality_gate.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| """判官闸口配置。""" | ||
|
|
||
| from pydantic_settings import BaseSettings, SettingsConfigDict | ||
|
|
||
|
|
||
| class QualityGateSettings(BaseSettings): | ||
| """判官闸口开关。环境变量前缀 ``QUALITY_GATE_``。 | ||
|
|
||
| 两个开关分开,是因为它们各自的代价不同,不该被一个 flag 绑在一起。 | ||
| """ | ||
|
|
||
| model_config = SettingsConfigDict( | ||
| env_prefix="QUALITY_GATE_", | ||
| env_file=("../.env", ".env"), | ||
| env_file_encoding="utf-8", | ||
| extra="ignore", | ||
| ) | ||
|
|
||
| # 每交付一个动作多打一次付费模型调用,所以默认不开:开它是一次花钱的决定。 | ||
| enabled: bool = False | ||
|
|
||
| # 判官说有问题就不交付。默认关,而且在积够 shadow 数据、定出判据之前不该开: | ||
| # 误杀掉的是用户**已经付过钱**的产物,退不回来;而漏放一个坏产物,用户可以重试。 | ||
| enforce: bool = False | ||
|
|
||
|
|
||
| settings = QualityGateSettings() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Pass the custom action description to the judge
When
action_typeisCUSTOM, the API requires and the generator usesinput.custom_prompt(for example,wave hello), but this call passes only the enum value"custom"into the judge prompt. The model therefore cannot evaluate whether the frame matches the requested custom action; withQUALITY_GATE_ENFORCE=trueit will commonly reportaction_matches=falseand block otherwise valid custom-action tasks, while shadow metrics for all custom actions are meaningless. Pass the custom prompt for custom actions and retain the enum label for the built-in actions.