Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions backend/packages/ai_engine/src/windup_ai_engine/master_prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
**核心规律(三次实测验证,写死为契约):母版姿态决定动作,提示词只能微调。**
- walk:母版**朝侧向**才不转身;正面母版配侧走词 → 模型靠转身调和图文矛盾。
- jump:母版**顶部留白**才不被视频画面裁掉。
- attack:必须给**极限蓄力母版**(出手那只手已拉到身后腰际)。用站立母版时,即使提示词
- attack:必须给**极限蓄力母版**(发力那一侧已拉到待发位)。用站立母版时,即使提示词
写死"不过头顶 / 不转身 / 只做一次",模型仍会抡过头顶、转到背面、劈两次 —— 强动作
先验压不住;换蓄力母版后模型只能"接着往前挥",没有再抡起的空间。
先验压不住;换蓄力母版后模型只能"接着往前发力",没有再抡起的空间。
蓄力姿态按运动拓扑分四支(见 :data:`ATTACK_MASTER_POSES`):同一张横挥蓄力母版
喂给直刺 / 远程 / 前扑,模型会先把收好的那一侧重新抡起来再做。

**姿势描述里不写装备名词(#195)。** 这几段是拿去生成母版的提示词,写"the weapon"等于
断言角色持械 —— 空手角色会被凭空塞一把武器,而母版是整条 i2v 链的身份来源,污染会一路
Expand All @@ -27,16 +29,26 @@

from PIL import Image

from windup_common.models import AttackArchetype

from windup_ai_engine._subject import bg_color as _bg_color
from windup_ai_engine.prompt._md import load_section

__all__ = ["add_headroom", "prepare_master", "MASTER_POSES"]
__all__ = ["add_headroom", "prepare_master", "MASTER_POSES", "ATTACK_MASTER_POSES"]

# 空值 = 该动作用中性站立母版即可。这是唯一允许空提示词的地方,故显式放行 ——
# 别处的空串会一路跑到付费调用。
MASTER_POSES = {
a: load_section("master_poses.md", a, allow_empty=True)
for a in ("walk", "run", "idle", "jump", "attack")
for a in ("walk", "run", "idle", "jump")
}

# attack 按运动拓扑取母版姿态:四支的起手姿态互不兼容(横挥蓄力母版跑不出直刺),
# 而"母版姿态决定动作"对 attack 最狠 —— 见本模块开头。这里不放行空值:
# 四支都必须有自己的蓄力姿态,缺一支就该炸,不能退回中性站立。
ATTACK_MASTER_POSES = {

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] 让拓扑专用母版姿态进入生成路径

这张新映射目前只被测试导入,生产代码没有任何消费者;VideoFrameStrategy.derive 仍直接使用同一份调用方 master,并且只按动作字符串调用 prepare_master(master, "attack"),完全不知道所选 archetype。于是即使直接构造 ActionSpec(archetype=LUNGE/PROJECT/...),运行时也只是换了视频提示词,不会使用这里声明的互不兼容的起手姿态,正好违背本 PR 所述“母版姿态决定动作”的核心约束。请在母版生成/选择链路中按 archetype 消费该映射,或把对应姿态随动作规格明确传入。

arch: load_section("master_poses.md", f"attack.{arch.value}")
for arch in AttackArchetype
}


Expand Down
21 changes: 21 additions & 0 deletions backend/packages/ai_engine/src/windup_ai_engine/prompt/_framing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""所有动作共用的构图约束。

由代码统一追加而不是抄进每份 md:同一条约束抄 N 份会各自漂移。

只写正向计数句 —— 该 i2v 接口没有 negative_prompt,否定句里的名词会被 latch 进画面
(实测"do not add dust"反而勾出更多灰尘),所以说"恰好一个",不说"不要第二个"。
"""
from __future__ import annotations

__all__ = ["SINGLE_SUBJECT_FRAMING", "with_framing"]

# 攻击的两处留白(母版姿态要求 + 母版补边)让画面空得足以容下第二个主体。
SINGLE_SUBJECT_FRAMING = (
"Exactly one character is in the frame, alone against a plain flat solid-color background, "
"and the whole body stays inside the frame."
)


def with_framing(body: str) -> str:
"""给一段动作正文接上构图约束。"""
return f"{body} {SINGLE_SUBJECT_FRAMING}"
18 changes: 13 additions & 5 deletions backend/packages/ai_engine/src/windup_ai_engine/prompt/actions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""待机 / 攻击 i2v 提示词。

提示词正文在 ``prompts/idle.md`` 与 ``prompts/attack.md``(#233)。
本模块只留加载与按 facing 分流。
本模块只留加载与按 facing / archetype 分流。
"""
from __future__ import annotations

from windup_common.models import Facing
from windup_common.models import AttackArchetype, Facing

from windup_ai_engine.prompt._framing import with_framing
from windup_ai_engine.prompt._md import load_section

__all__ = ["build_idle_prompt", "build_attack_prompt"]
Expand All @@ -16,11 +17,18 @@ def build_idle_prompt(facing: Facing | str = Facing.SIDE) -> str:
"""待机正文(循环类)。``facing`` 须与母版朝向一致。

"""
return load_section("idle.md", Facing(facing).value)
return with_framing(load_section("idle.md", Facing(facing).value))


def build_attack_prompt(facing: Facing | str = Facing.SIDE) -> str:
def build_attack_prompt(
facing: Facing | str = Facing.SIDE,
*,
archetype: AttackArchetype | str = AttackArchetype.THRUST,
) -> str:
"""攻击正文(一次性类)。``facing`` 须与母版朝向一致。

默认取 THRUST:四支里只有 SWEEP 要求手里有一件有宽面的长条物,拿它当默认 = 对每个未知角色断言持械(#195)。
"""
return load_section("attack.md", Facing(facing).value)
# 两个枚举都过一遍构造:非法值要炸,不能静默落到某一节。
section = f"{AttackArchetype(archetype).value}.{Facing(facing).value}"
return with_framing(load_section("attack.md", section))
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from windup_common.models import Facing

from windup_ai_engine.prompt._framing import with_framing

__all__ = ["build_custom_prompt", "MAX_ACTION_CHARS"]

# 不是接口限制,是产品判断:描述越长越容易夹带角色外观,而外观由母版承载,写两遍会打架。
Expand Down Expand Up @@ -68,4 +70,4 @@ def build_custom_prompt(
lock = _FACING_LOCK[Facing(facing)] # 非法朝向要炸,不静默落到某一支
tail = _CYCLIC_TAIL if cyclic else _ONESHOT_TAIL
# 朝向放最前:最强的约束先钉。
return f"The character {lock}: {text}, {_KEEP_WHAT_IT_HAS}. {tail}"
return with_framing(f"The character {lock}: {text}, {_KEEP_WHAT_IT_HAS}. {tail}")
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from windup_common.models import Facing

from windup_ai_engine.prompt._framing import with_framing
from windup_ai_engine.prompt._md import load_section

__all__ = ["JUMP_PHASES", "build_jump_prompt"]
Expand All @@ -23,4 +24,4 @@ def build_jump_prompt(facing: Facing | str = Facing.SIDE) -> str:
facing: :class:`Facing` 成员(或其等价字符串),**必须与母版朝向一致**。

"""
return load_section(_DOC, Facing(facing).value)
return with_framing(load_section(_DOC, Facing(facing).value))
Original file line number Diff line number Diff line change
@@ -1,22 +1,96 @@
# 攻击 i2v 提示词(一次性类)

## side
节名是 `<运动拓扑>.<朝向>`。分支依据是**身体怎么发力**,不是手里拿着什么:
写"宽面""弧线"这类形状词等于断言角色手握一件有宽面的长条物,喂法杖 / 空手 / 四足角色时
模型会凭空补出那件东西来调和图文矛盾(与 #195 同一个坑,只是从名词层退到形状层)。

四支都要写整体位移(whole body / torso / hips):i2v 强跟身体、弱跟持物,
只描述持物的运动会让它自行漂移。

## sweep.side

```text
Seen from the side facing right, the character makes ONE single committed strike, staying in STRICT SIDE VIEW the whole time:
starting coiled with the weight on the back foot, the whole body uncoils and the hips drive forward as the weight surges onto the front foot,
the striking side of the body travelling in one continuous path from far behind the body, down across the front of the torso, out to full extension low in front,
whatever the character already wears or carries keeps its own shape and moves with the body, anything held in the hands stays in the same grip at the same angle,
then the torso settles back upright into guard and holds that stance.
The torso and hips keep pointing to the right the entire time and the character never turns toward or away from the viewer.
```

## sweep.front

```text
Facing the viewer, the character makes ONE single committed strike: starting coiled with the weight on the back foot,
the whole body uncoils forward and the hips turn into the motion as the weight surges onto the front foot,
the striking side of the body travelling in one continuous path across the front of the torso out to full extension,
whatever the character already wears or carries keeps its own shape and moves with the body, anything held in the hands stays in the same grip at the same angle,
then the torso settles back upright into guard and holds that stance, standing steady and keeping FACING THE VIEWER.
```

## thrust.side

```text
Seen from the side facing right, the character drives ONE single committed strike straight forward, staying in STRICT SIDE VIEW the whole time:
starting coiled low with the weight on the back foot and the striking side pulled in at waist height,
the hips snap forward and the whole body drives straight ahead as the weight lands on the front foot,
the striking side of the body extending in one straight line directly forward to full reach and stopping there,
whatever the character already wears or carries keeps its own shape and moves with the body, anything held in the hands stays in the same grip at the same angle,
then the torso draws back over the hips and settles into guard and holds that stance.
The torso and hips keep pointing to the right the entire time and the character never turns toward or away from the viewer.
```

## thrust.front

```text
Facing the viewer, the character drives ONE single committed strike straight toward the viewer:
starting coiled low with the weight on the back foot and the striking side pulled in at waist height,
the hips snap forward and the whole body drives straight ahead as the weight lands on the front foot,
the striking side of the body extending in one straight line directly toward the viewer to full reach and stopping there,
whatever the character already wears or carries keeps its own shape and moves with the body, anything held in the hands stays in the same grip at the same angle,
then the torso draws back over the hips and settles into guard and holds that stance, standing steady and keeping FACING THE VIEWER.
```

## project.side

```text
Seen from the side facing right, the character makes ONE single committed ranged release, staying in STRICT SIDE VIEW the whole time:
starting settled with the weight low over both feet, the torso presses forward over the front foot and the hips square up behind the motion,
the releasing side of the body reaching straight out in front of the chest and coming to a firm stop at full extension,
the whole body braced and steady at that moment,
whatever the character already wears or carries keeps its own shape and moves with the body, anything held in the hands stays in the same grip at the same angle,
then the character keeps that extended pose and stays still.
The torso and hips keep pointing to the right the entire time and the character never turns toward or away from the viewer.
```

## project.front

```text
Facing the viewer, the character makes ONE single committed ranged release toward the viewer:
starting settled with the weight low over both feet, the torso presses forward and the hips square up behind the motion,
the releasing side of the body reaching straight out in front of the chest toward the viewer and coming to a firm stop at full extension,
the whole body braced and steady at that moment,
whatever the character already wears or carries keeps its own shape and moves with the body, anything held in the hands stays in the same grip at the same angle,
then the character keeps that extended pose and stays still, keeping FACING THE VIEWER.
```

## lunge.side

```text
Seen from the side facing right, the character makes ONE single committed attack, staying in STRICT SIDE VIEW the whole time:
starting coiled with the weight on the back foot, the body leans forward and the weight surges onto the front foot,
the leading arm sweeping through one smooth downward crescent arc from high behind the shoulder down across the front to full extension low,
anything held in that hand keeping its exact length and grip position and staying clearly in front of the body with its broad side facing the viewer the whole way,
whatever the character already wears swinging with the motion, then the body settles back upright into guard and holds that stance,
standing steady. The torso and hips keep pointing to the right the entire time and the character never turns toward or away from the viewer.
Seen from the side facing right, the character makes ONE single committed lunge forward, staying in STRICT SIDE VIEW the whole time:
starting crouched low with the weight loaded onto the rear limbs, the whole body surges forward in one burst with the head and the leading limbs arriving first,
the hips and torso following along that same line and the back stretching out level and low over the ground,
whatever the character already wears or carries keeps its own shape and moves with the body,
then the body gathers back under itself, settles low and holds that crouched stance.
The torso and hips keep pointing to the right the entire time and the character never turns toward or away from the viewer.
```

## front
## lunge.front

```text
Facing the viewer, the character makes ONE single committed attack: starting coiled with the weight on the back foot,
the whole body uncoils forward, the leading arm sweeping through one smooth arc across the front to full extension,
anything held in that hand keeping its exact length and grip position and staying clearly in front of the body with its broad side facing the viewer the whole way,
whatever the character already wears swinging with the motion, then the body settles back upright into guard and holds that stance,
standing steady and keeping FACING THE VIEWER.
Facing the viewer, the character makes ONE single committed lunge toward the viewer:
starting crouched low with the weight loaded onto the rear limbs, the whole body surges forward in one burst with the head and the leading limbs arriving first,
the hips and torso following along that same line and the back stretching out level and low over the ground,
whatever the character already wears or carries keeps its own shape and moves with the body,
then the body gathers back under itself, settles low and holds that crouched stance, keeping FACING THE VIEWER.
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# 各动作所需的母版姿态

attack 按运动拓扑分四节(`attack.<拓扑>`):母版姿态决定动作、提示词只能微调,
而四支的起手姿态互不兼容 —— 拿横挥蓄力母版跑直刺,模型会先把收在腰际的那侧重新抡起来。

## walk

## run
Expand All @@ -13,10 +16,36 @@ deep crouch coiled to spring straight upward: the knees bent low and the hips su
the weight loaded onto both legs at the very moment before springing straight up, anything held in the hands kept in a fixed grip; leave generous empty space above the head
```

## attack
## attack.sweep

```text
extreme wind-up stance for a horizontal strike: the striking hand drawn far BACK behind the body at WAIST height,
the torso twisted back and coiled, weight fully loaded on the back leg, both arms low and pulled back,
that hand and anything held in it staying BELOW the shoulders; leave generous empty space on the swing side
```

## attack.thrust

```text
low coiled stance ready to drive straight forward: the weight sunk onto the back leg with both knees bent,
the striking side pulled in tight against the body at WAIST height and held there ready to fire,
the torso squared low over the front foot, that side and anything held in it staying BELOW the shoulders;
leave generous empty space in front
```

## attack.project

```text
braced stance ready to send something forward at a distance: both feet planted wide and firmly set,
the hips sunk low and the weight centred between the feet, the torso upright and square,
both hands drawn in close in front of the chest and held there, anything held in them kept in a fixed grip;
leave generous empty space in front
```

## attack.lunge

```text
crouched stance coiled to spring forward: all four limbs folded under the body with the chest lowered close to the ground,
the rear limbs deeply loaded and ready to drive, the head and the leading limbs pointing forward along the line of travel,
anything the character carries kept in place; leave generous empty space in front
```
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from windup_common.models import Facing

from windup_ai_engine.prompt._framing import with_framing
from windup_ai_engine.prompt._md import load_section

__all__ = ["build_walk_prompt"]
Expand All @@ -22,4 +23,4 @@ def build_walk_prompt(facing: Facing | str = Facing.SIDE) -> str:

"""
# 过一遍 Facing() 构造:非法值要炸,不能静默落到某个模板。
return load_section(_DOC, Facing(facing).value)
return with_framing(load_section(_DOC, Facing(facing).value))
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,15 @@ def _build_prompt(self, action: ActionSpec) -> str:
facing=action.facing,
cyclic=bool(action.cyclic),
)
# attack 同样进不了那张表:它还要按运动拓扑选提示词分支。archetype 缺省时不在这里
# 兜一个默认值 —— 缺省只由 build_attack_prompt 定义一次,写两处会各自漂移。
if action.action is ActionType.ATTACK:
if action.archetype is None:

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] 把 archetype 贯通到生产请求模型

生产入口 CharacterActionGenerateRequestCharacterActionInputActionTaskExecutor._produce_action 都没有 archetype 字段或转发逻辑,所以所有 API 提交的 attack 最终都会走这里的 None 分支并固定使用默认 THRUST;客户端即使提交 archetype: "sweep"/"project"/"lunge",Pydantic 请求模型也会把这个未知字段忽略。请将该枚举从请求层贯通到 ActionSpec(并对非 attack 拒收),否则新增的三个非默认拓扑在实际产品路径中不可达。

return build_attack_prompt(facing=action.facing)
return build_attack_prompt(facing=action.facing, archetype=action.archetype)
builders = {
ActionType.JUMP: build_jump_prompt,
ActionType.IDLE: build_idle_prompt,
ActionType.ATTACK: build_attack_prompt,
}
build = builders.get(action.action, build_walk_prompt)
return build(facing=action.facing)
Expand Down
2 changes: 2 additions & 0 deletions backend/packages/common/src/windup_common/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
DEFAULT_N_FRAMES,
ActionSpec,
ActionType,
AttackArchetype,
CharacterCard,
CharacterView,
Facing,
Expand All @@ -11,6 +12,7 @@

__all__ = [
"ActionType",
"AttackArchetype",
"GenRoute",
"Facing",
"CharacterView",
Expand Down
26 changes: 26 additions & 0 deletions backend/packages/common/src/windup_common/models/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ class ActionType(str, Enum):
CUSTOM = "custom"


class AttackArchetype(str, Enum):
"""攻击的运动拓扑 —— 决定 ``prompts/attack.md`` 取哪一节(取值即节名前缀)。

按"身体怎么发力"分而不按装备形状分:提示词里的形状先验(宽面、弧线)等于断言角色
手握一件有宽面的长条物,喂空手 / 法杖 / 四足角色时模型会凭空补出那件东西来调和矛盾。
"""

SWEEP = "sweep" # 长条持物:横挥 / 下劈
THRUST = "thrust" # 短持物或空手:直出 / 戳刺
PROJECT = "project" # 远程:身体前压、送到位、终态保持
LUNGE = "lunge" # 非双足:整体前扑,头部 / 前肢领先


class GenRoute(str, Enum):
"""生成路线 —— 实测挣得的分流依据(见 ai_engine.strategy 层 docstring)。

Expand Down Expand Up @@ -191,6 +204,19 @@ class ActionSpec(BaseModel):
# pick_cycle 还是 pick_oneshot、出参要不要量 loop_seam。
cyclic: bool | None = None

# 攻击走哪一支运动拓扑。``None`` = 不指定,由 ``build_attack_prompt`` 的默认值决定 ——
# 这里不给默认值,否则同一个缺省被两处各写一份,改一处另一处静默不动。
archetype: AttackArchetype | None = None

@model_validator(mode="after")
def _archetype_belongs_to_attack_only(self) -> ActionSpec:
"""非攻击动作带 archetype 要炸:它只被攻击提示词消费,传了不会生效。"""
if self.action is not ActionType.ATTACK and self.archetype is not None:
raise ValueError(
f"action={self.action.value} 不该带 archetype;它只决定攻击提示词取哪一支,传了不会生效"
)
return self

@model_validator(mode="after")
def _custom_needs_its_own_settings(self) -> ActionSpec:
"""两个方向都卡:缺了只能猜、而猜错是静默的;多给了调用方以为能覆盖 walk 的
Expand Down
Loading
Loading