Skip to content

fix: resolve stale linked issue warnings - #16

Closed
fennoai[bot] wants to merge 91 commits into
mainfrom
fennoai/fix-pr-issue-warning
Closed

fix: resolve stale linked issue warnings#16
fennoai[bot] wants to merge 91 commits into
mainfrom
fennoai/fix-pr-issue-warning

Conversation

@fennoai

@fennoai fennoai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Requested by @minorcell

Summary

  • mark the existing missing-issue warning as resolved when GitHub reports a closing issue
  • distinguish active and resolved warning markers so future regressions can warn again
  • add regression coverage for stale, duplicate, and resolved warning comments

huyanxius and others added 30 commits August 11, 2026 13:43
* build(frontend): add React Flow dependency

The workflow editor needs a maintained graph renderer for its controlled canvas.

Add @xyflow/react and record its exact dependency closure in the lockfile.

The frontend can build the editor without loading graph code on unrelated routes.

* fix(character): align workflow run ownership

The backend binds every Character to the WorkflowRun that produced it.

Map and serialize workflow_run_id, then update typed fixtures that consume Character.

Editors can select the correct character without guessing from project order.

* docs(character): document workflow run ownership

The existing contract text omitted the backend ownership field and described a missing run property.

Document workflow_run_id and the action outfit lookup through the bound Character.

Future adapters have an accurate source of truth for character and outfit identity.

* feat(workflow-editor): compose real workflow session

The editor must recover one persisted WorkflowRun through the main repository interfaces.

Compose the controller with project context, the run-bound Character, and an explicit Generation failure boundary.

Production routes no longer depend on demo data or a second workflow state machine.

* feat(workflow-editor): render controlled workflow canvas

The production editor needs to project persisted workflow state without owning business transitions.

Render six system node types and parallel action branches through one controlled React Flow canvas.

Commands, recovery paths, and transient UI state remain bounded by WorkflowController and the active run.

* feat(app): lazy-load workflow editor route

React Flow should not increase the initial bundle cost of unrelated application routes.

Load the editor behind Suspense while preserving both protected WorkflowRun route forms.

Authenticated deep links keep their existing behavior and load the canvas only when requested.

* feat(workflow-controller): accept character setup input

Manual editor submissions need to persist the configured prompt before generation.

Allow configuring nodes to override their input while keeping completed nodes immutable.

Generation receives only canonical fields from the persisted setup snapshot.

* feat(workflow-editor): add character setup form

Manual WorkflowRun entry needs an editable prompt before the setup node is fixed.

Render a controlled description field and submit its value through WorkflowController.

The form becomes read-only after persistence and keeps the existing visual system.

* test(workflow-editor): cover character setup submission

The manual setup path must persist edited input without changing the generation payload shape.

Exercise the real page and controller from form entry through the fixed setup state.

Exact payload assertions prevent UI-only success and accidental nested fields.

* docs(workflow-editor): add PR screenshots

The UI change needs reviewer-visible desktop and mobile evidence.

Add screenshots from the current canvas renderer with blank image placeholders.

Reviewers can verify the six-node layout without shipping screenshot mock code.

* feat(workflow-editor): publish reviewed character actions

Approved animations must become durable Character assets before review state advances.

Upsert the reviewed branch into its existing Outfit through the Character API boundary.

Retries keep one stable Action ID and reject mismatched Generation results.

* test(export): cover reviewed action publishing

Character publication needs contract coverage for the nested asset tree.

Exercise action mapping, retry replacement, and Generation ownership guards.

The tests prevent duplicate or cross-workflow assets from being persisted.

* test(workflow-editor): cover asset publication order

Review approval must not advance before the Character asset is durable.

Exercise the session orchestration and the page command boundary.

The tests keep publication failures from producing a passed review node.

* fix(workflow-editor): make asset publication state explicit

Character publication should not rely on unrelated WorkflowRun or busy-state renders.

Return the published Character to page state and limit action updates to the asset tree.

The editor preserves character metadata and removes the unused export surface.

* test(workflow-editor): cover explicit character refresh

Asset publication needs regression coverage independent of review persistence.

Exercise preserved character metadata and page state after an approval failure.

The tests prevent getter-based refresh coupling from returning.

* fix(workflow-editor): scope canvas state to nodes, roles and branches

Generation results were keyed by node id alone, so a node holding two tasks silently kept only the last one. Deleted nodes still issued reads, every command reset the viewport, and a single busy slot let a second branch unlock the first mid-flight.

Key generation reads by node and role, skip deleted nodes, refit the view only when the node set changes, and hold busy branches in a set. Four ad-hoc race guards collapse into one AbortController per session and one per read.

Regression tests cover each case, including two parallel branches keeping their own locks. Pass-through wrappers and swapped variable names are gone.

* refactor(workflow-editor): move page styles to tailwind utilities

The editor shipped the only page-level stylesheet in the frontend, while index.css states that everything outside it goes through Tailwind utilities. It also carried its own palette of near-identical greens spread across the sheet and the node data.

Move layout, card, menu and boundary styles onto the elements as utility classes. The stylesheet keeps only what utilities cannot express: the shared colour variables, React Flow internal overrides, edge states and keyframes.

The sheet drops from 487 to 76 lines with no visual change, verified by comparing rendered screenshots before and after. Nested main landmarks become plain containers.

* perf(workflow-editor): reuse settled generation results across run updates

The controller emits on every persist, and the page answered each emit by re-reading every generation the run refers to. With four action branches a single click issued a dozen GETs for results that had not changed.

Keep settled results in a map keyed by task id and serve them from there. Tasks still pending or running are read again, since their status is what the canvas is waiting on, and the map is dropped when the run changes.

A probe over one main line plus three action branches drops from seven reads per command to zero, while the first load still costs seven.
…Engineer#168)

* ci(coverage): add pure GitHub Actions coverage reporting

- Add pytest-cov>=5.0 to dev dependencies
- Add [tool.pytest.ini_options] with --cov=packages --cov-report=term-missing
- Pytest step: add --cov-report=xml and --cov-report=html flags
- Upload HTML coverage report as Artifact (30-day retention)
- PR auto-comment with line/branch coverage summary via github-script
- Update existing comment instead of creating duplicates
- Add pull-requests: write permission for PR comments

Refs: 1024XEngineer#167

* ci(coverage): split PR comment into separate workflow_run

- Move PR comment from backend.yml to new coverage-comment.yml
- Use workflow_run trigger to get write permissions on fork PRs
- Backend CI uploads coverage.xml as artifact (1-day retention)
- Coverage Comment workflow downloads artifact and posts PR comment
- Fixes 'Resource not accessible by integration' 403 error

Refs: 1024XEngineer#167

* ci(coverage): add issues:write permission for PR comment

github.rest.issues.listComments/updateComment/createComment requires
issues:write, not just pull-requests:write.

Refs: 1024XEngineer#167

* fix(ci): resolve three coverage workflow issues

- Regenerate uv.lock to include pytest-cov (fixes uv sync --frozen)
- Use workflow_run.pull_requests[0].number for PR lookup (fixes fork PR)
- Merge download/parse/comment into single step (fixes artifact-not-found)
- Remove emoji from coverage comment

Refs: 1024XEngineer#167

* ci(coverage): replace custom coverage comment with Codecov integration

- Add codecov/codecov-action@v5 to backend.yml for coverage upload
- Remove coverage-comment.yml (superseded by Codecov native PR comments)
- Add codecov.yml with informational status and backend flag config

Refs: 1024XEngineer#167

* ci(coverage): remove HTML artifact upload and add frontend flag

- Remove HTML coverage report artifact step (Codecov replaces it)
- Remove --cov-report=html:htmlcov from pytest command
- Add frontend flag to codecov.yml comment config
* feat(media): add validated upload adapter

* fix(media): preserve late upload cancellation

* chore(media): keep pull request scope source-only

* refactor(media): reuse authenticated API client

* fix(media): align authenticated upload adapter
…gineer#198)

* feat(workflow-editor): create character on template confirmation

New WorkflowRuns can confirm a character template before any Character asset exists.

Create the bound Character, seed its default outfit, and return it to the editor before advancing the template node.

Keep subsequent action generation on the real Character and cover the flow with runtime and page regressions.

* test(workflow-editor): cover character template confirmation branches

Template confirmation has validation and idempotent paths that lacked behavior coverage.

Exercise invalid selection inputs and the existing Character with Outfit path.

Keep patch coverage focused on observable session behavior.

* fix(character): make workflow character creation atomic

Concurrent editor sessions could persist multiple Characters for one WorkflowRun.

Enforce database uniqueness and recover conflicts through a project-scoped get-or-create path.

Keep duplicate creation idempotent without exposing Characters across projects.
…er#171) (1024XEngineer#172)

* feat(common): 角色卡与动作规格共享 DTO

跨层契约,无内部依赖。ai_engine 与 app 均依赖此,避免两层各自定义同名结构。

- CharacterCard  角色身份(一致性主键,资产库基础)
- ActionSpec     动作规格(帧数 / 帧率 / 循环模式 / 风格化 / 朝向)
- ActionType     idle / walk / run / jump / attack / hit
- GenRoute       video_i2v / per_frame

GenRoute 只列有实现的路线。没有实现的枚举值等于死代码:它让调用方以为该能力存在,
而分流到它只能得到运行时错误。未来路线(三渲二渲染出帧)的契约需求记在 1024XEngineer#81 / 1024XEngineer#122,
随实现一起加成员——枚举加成员是纯加法,不构成破坏性变更。

Refs 1024XEngineer#53

* refactor(common): 受限取值改枚举,n_frames 独立,删掉零消费方的 palette

本 PR 是 1024XEngineer#152 的前置:它的 `ai_engine/ports` 与 `strategy/concrete` 都 import
`windup_common.models` 的 `CharacterCard` / `ActionSpec`,而那两个符号在主线上不存在
(`models/` 下只有 `.gitkeep`),合入即 import 失败。

## 为什么受限取值一律用枚举

`facing` 承载一条实测挣得的硬约束——提示词朝向必须与母版朝向一致(给正面母版喂侧走词,
模型会靠转身调和图文矛盾)。它此前是裸 str、合法值只写在行尾注释里:写成 "Side" /
"sidee" 不报错、不告警,调用链一路放行,几分钟和一次真金白银的视频调用之后才在画面上
看出角色转了身。枚举把这类错误从"生成完靠肉眼发现"提前到"构造 ActionSpec 时
ValidationError",成本从一次付费生成降到零。`loop` / `stylize` / `view` 同理。

`view` 的取值与前端契约(frontend/API_CONTRACT.md 的映射表)逐字一致,免得将来做
int ↔ str 映射时再造一套别名(topdown / top_down / top-down 三写)。

## extra="forbid"

理由与用枚举同源:字段名也是靠字符串传递的约束。`ActionSpec(action=..., n_frame=16)`
(少个 s)在 pydantic 默认的 extra="ignore" 下不报错、不生效,调用方以为要了 16 帧、
实际拿到默认 8 帧。已删字段(如 palette)同理会被静默吞掉。

## n_frames 独立成字段

原先由 `len(poses)` 推导,但视频路线根本不读 poses——推导意味着"想要 16 帧就得先编
16 条用不上的姿势描述",而那 16 条描述读者会以为真的进了提示词。
只传 poses 的旧调用方零改动(回退到 len(poses));两个都给且不等时抛错而不是猜一个——
common 层看不到 ROUTE_MATRIX,判不出走哪条路线,猜的代价是静默出错帧数。

## 删掉 palette

它会变成"看起来生效、实则被忽略"的第二真相源:真正锁色的色板由
postprocess.master_pixel_spec 从母版像素里量出来,而这个字段零消费方、无格式约定。
调用方填了 "#1a1a2e,#e94560" 期待锁色,管线照旧用母版色板,不报错也不生效。
将来若要支持用户指定色板,连同消费它的代码一起加回,并用结构化类型而非自由 str。

## 数值下界抄的是实现里已有的真实取值域

把"实现悄悄纠正入参"提前成入参报错:`pixel_h` → to_pixel_art 对 <1 直接 raise;
`palette_size` → `quantize(colors=max(2, palette_size))` 会把 1 静默抬成 2,于是
"我要 1 色"拿到 2 色且无任何提示;`fps` → 0 对播放侧是除零/静止,没有合法语义。

Refs 1024XEngineer#171
Refs 1024XEngineer#53

* refactor(common): 删掉两个接了不履约的入参,并给契约补自带测试

机器审在本 PR 报的问题指向同一件事:契约里存在"能填、但填了不生效"的字段。本 PR 自己的
GenRoute docstring 已经写了这条原则("只列有实现的路线;没有实现的枚举值等于死代码"),
只是没对 ActionSpec 执行。

删除:
- ActionSpec.fps —— 零写入方(编排层构造 ActionSpec 时从不传),而 postprocess.
  frame_durations 按动作查表、根本不看它。留着的后果是同一段素材有两个互相矛盾的播放
  速度:fps=20 宣称 50ms/帧,walk 实际给 125ms/帧。播放时序的唯一真相源定为出参的
  durations(逐帧 ms),比单一帧率严格更能表达(关键帧定格)。
- ActionSpec.loop 与 LoopMode 枚举 —— 零消费方。闭环行为写死在 slicing.pick_cycle:
  循环类动作一律抽单周期闭环,传 pingpong / none 不改变任何产出。调用方能为一段往返
  动画付费、拿到一段线性循环,正是本项目最忌讳的静默成功。此前的处理是写注释说明"别
  指望它生效",并写了一条把"三种 loop 产出相同"钉成事实的测试 —— 那是把缺陷固化,不是
  修。真要支持 pingpong,连同 pick_cycle 的分支与出参时序契约一起加回。

补文档:
- ActionType docstring 显式声明本枚举是"引擎能生成的动作",与入口侧
  orchestrator.model.ActionType(另有 custom,少 run/jump/hit)刻意分离,跨越靠编排层
  的显式适配函数 _to_engine_action。不把 custom 加进来的理由与上面同一条:ROUTE_MATRIX
  没有它的分流,加成员等于接收一个无法履约的请求。API 入口枚举不变,既有调用方兼容性
  不受影响。

补测试(本 PR 此前零测试,而它是其余分片的硬前置):
- 新增 tests/test_character_contract.py,35 条,只测 DTO 自身、不 import 上层包 ——
  契约包要能独立验证。提示词构造器那几条依赖 ai_engine,随实现分片走。
- 覆盖:受限取值拒绝拼写错误、字段名打错不被静默丢弃(extra="forbid")、n_frames 与
  poses 自相矛盾时抛错而非二选一、取值域下界、以及 palette / fps / loop 三个已删字段
  传入时必须听得见响。
- 做过变异测试:把 fps 加回去 2 条红,把 loop + LoopMode 加回去 3 条红。

---------

Co-authored-by: johnnyzhang-eng <johnnyzhang-eng@users.noreply.github.com>
…4XEngineer#110)

* feat(generation): add compatible authenticated SSE adapter

* test(generation): cover adapter protocol boundaries

* fix(generation): reconcile SSE event contracts

* fix(generation): derive ownership from access token

* refactor(auth): remove client user id contracts
…Refs 1024XEngineer#171) (1024XEngineer#179)

* feat(framework): 补 provider 抽象接口与抠图/视频实现

providers/ 此前只有三个 create_*_client 工厂,没有可供上层依赖的抽象类型,
ai_engine 无法在不 import 具体实现的前提下声明它需要什么能力。

- interfaces.py:ImageProvider / VideoProvider / MatteProvider 三个 Protocol,
  零依赖,供上层按能力而非按厂商声明依赖。
- matte.py:OnnxU2NetMatteProvider,onnxruntime 直跑 u2netp。不用 rembg:其底层
  同样依赖 onnxruntime,且 numba 老链在 3.12 无轮子。onnxruntime 导入失败时降级
  到 Pillow 兜底而非崩溃。
- sufy.py:SufyImageProvider / SufyVideoProvider。视频成品下载加三次退避重试与
  长度校验 —— 该步发生在提交任务、轮询、等待全部成功之后,此时费用已产生、视频
  已生成好,只差取回数据,连接断一次整单作废。实测同一角色连续两单死在这里各烧
  一次费用。test_sufy_video_download 的四条断言拿修复前的旧实现做过对照,确认其中
  三条在修复前会失败。

依赖声明:
- qiniu>=7.14 —— 此前未声明,镜像能起、/docs 也 200,只有第一次 POST /media/upload
  才 ModuleNotFoundError。
- onnxruntime>=1.17,<1.24 —— 1.24 起不再发布 macOS Intel(x86_64) wheel,Intel Mac
  装不上。1.23.x 仍覆盖 Intel/arm64/Linux + py3.12,API 一致,抠图代码零改动。

本 PR 不依赖其他未合分支:providers 不 import windup_common.models。

* fix(providers): 首帧字段按模型选,并拦截被静默忽略的参考图

2026-08-07 用一张全新角色母版跑 kling-v3-omni 端到端时实测发现,费用已产生。

现象:提交成功、status=completed、16 帧齐、逐帧时长齐、下游抽帧/选帧/抠图/脚线对齐
全部正常工作,最终产出一组构图完整的序列帧。但画面里是一个**与母版毫无关系的写实路人**
——母版是插画风、赭黄长外套、背铜管乐器的乐手,产出是深绿外套的写实人物,且只有下半身
(提示词里 "the legs clearly visible" 被当成了取景指令)。

根因:首帧字段按**模型**选,不是按"本地图/公网 URL"选。厂商文档写明 Kling 用
image_list、Sora 用 input_reference,而本仓只把 kling-video-o1 列进了 image_list 名单。
kling-v3-omni 收到 input_reference 后既不报错也不采纳,退化成纯文生视频。

危险在于失败形态:老模型(v2 系列)塞错字段会 failed,还能发现;kling-v3-omni 是
**成功返回一个错误结果**,整条管线无一处能察觉。这与本批 PR 已修的"未实现路线返回空帧"
属同一类问题,只是发生在更外层——空帧至少还能靠"帧是空的"判出来,这个连帧都是好的。

两处修复:

1) _needs_image_list 显式归类 + kling-v3 前缀兜底。仅对已确认的型号切换字段:
   v2-5-turbo / v2-1 已实测可吃 input_reference(2026-07-27 端到端到 completed),
   不动既有通路,避免为修一个模型而破坏三个。

2) _assert_reference_registered 在**下载视频之前**拦截。网关在
   billing_type_description 里明写计费口径,送了首帧却拿到"无参考视频"即为铁证。
   提交后与轮询到 completed 时各查一次。字段缺失时不拦——不同网关字段不一定存在,
   宁可漏判也不误伤。

四条回归测试,变异测试确认有效:把 v3-omni 退回 input_reference(复现原 bug)、
去掉计费口径检查,各有 1 条用例失败;还原后 8 passed。

* feat(providers): 按现行 FAL 队列接口重写 i2v,并回退按旧接口形状打的两处补丁

2026-08-07 拉网关 OpenAPI spec 逐个核对:平台现有 69 个 POST 视频端点,其中 22 个
图生视频**全部**在 FAL 队列面 /queue/... 下,首帧一律是 URL 形态字段(image_url /
start_image_url),同日实测送 base64 dataURI 无一能用。原 SufyVideoProvider 建在
OpenAI 风格 /v1/videos + input_reference dataURI 上,是过时的接口形状——在它上面打的
两处补丁方向错了,一并回退:

- _needs_image_list / _IMAGE_LIST_MODELS 里新增的 kling-v3-omni / kling-v3
- _assert_reference_registered / ReferenceIgnoredError 及其 3 条测试

新增 FalQueueVideoProvider 与旧实现并存(没有实测证据说 /v1/videos 已坏,sora 系可能
仍只在那一面)。要点:

1) 模型 → 端点的显式硬表 FAL_I2V_ENDPOINTS,不拼路径。每家有三样东西不同且都猜不出
   来:提交路径的型号段;首帧字段名(同是 kling,o3 / v2.5-turbo 叫 image_url,
   v3 / v2.6 / o1 叫 start_image_url);轮询前缀(**不是**提交路径 + /requests,
   kling 六个型号共用 /queue/fal-ai/kling-video/requests/{id})。未登记的模型抛
   UnknownVideoModelError,不做前缀匹配、不做兜底——猜出一条"存在但语义不同"的路径
   (如把 image-to-video 猜成 reference-to-video)会正常出片、正常计费。

2) i2v 契约冲突:Protocol 收 bytes,FAL 面只吃公网 URL。选择"provider 自己适配",
   Protocol 签名不动——新增 FirstFrameUploader port,provider 构造时必传,内部把补边
   后的首帧换成 URL。调用方零改动;母版已在公网时用 PreUploadedFirstFrame 复用该
   URL、不重传。

3) 失败一律显式抛错,不静默降级:spec 明写「任务失败时后端也返回 COMPLETED,通过
   detail 区分」,故 COMPLETED 还要查 detail;认不出的 status 当失败(继续轮询会把
   "协议变了"伪装成"生成太慢");超时抛 VideoJobTimeoutError;参数校验在上传首帧之前
   完成;下载复用既有 _download(重试 + 长度校验,治"视频已生成、费用已产生,下载断
   一次整单作废")。

FAL 面鉴权是 Authorization: Key(不是 Bearer),base_url 需从 /v1 退回网关根
(/queue 与 /v1 平级)。两处都有 spec 依据,已写进注释与测试。

37 条新测试全程 mock 不联网;11 个变异(错端点 / 错字段名 / 错轮询前缀 / 去掉各处抛错
/ 去掉下载重试 / 参数校验挪到上传后)逐个确认能被测到,全部 KILLED。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(providers): 抠图补底色清理,去掉猜背景色的静默兜底

两处,都是 2026-08-07 用三个全新角色母版实测出来的。

1) u2netp 对闭合区域天然失灵
   四足角色腿间的背景是一块被主体围住的空隙,显著性模型把它当成主体内部,整块底色
   留在产物里;轮廓上还带一圈底色描边。母版底色是刻意生成的纯色、均匀度极高(实测
   四角标准差 1.0–1.2),拿它做一次窄阈值清理正好补上这个洞。

   阈值必须窄。实测一个铁锈橙毛 (222,130,70) 的角色配玫红底 (222,41,124):两者红通道
   完全相同、欧氏距离仅 104。先后试过两版宽阈值 chroma,都把橙毛判成半透明并去"反解",
   越解越坏(先成橄榄绿、再成亮绿)。取 38 时橙毛 d≈117 完全不受影响,而闭合空隙里的
   背景 d≈0 干净移除。三个角色残留 2.54%/0.44%/1.25% → 0.17%/0.21%/0.26%。

   与"按颜色抠是死路"那条规则的边界:那条说的是拿颜色当**主体判据**(白底浅色角色会
   被抠穿)。这里主体判据仍是 u2netp,颜色只用来**做减法**,绝不新增主体像素;底色不够
   均匀时(四角 std > 8)直接跳过,等于不清理。

2) 去掉 onnxruntime 缺失时的静默兜底
   旧行为是回落到"取四角主色做 chroma-key"。两个问题:猜背景色——白底母版四角就是白色,
   浅色角色与背景撞色会被抠穿;静默——开发机上看着能跑、输出其实是坏的,要到产物验收
   才发现。改为抛 RuntimeError。

五条回归测试,变异测试确认有效:阈值放宽到 120(误伤橙毛)、去掉均匀性守卫、清理系数
允许 >1(凭空造主体)、恢复静默兜底,各有用例失败;还原后 7 passed。

* fix(framework): 依赖声明取主线版本,修 rebase 时 lock 与 pyproject 不一致

rebase 到 main 时解冲突取了主线的 uv.lock,但 framework/pyproject.toml 取了本分支的,
后者少了主线用户模块加的 passlib[bcrypt] / redis / resend —— CI 装依赖时按 pyproject
解析,于是 conftest.py 导入 bcrypt 失败(ModuleNotFoundError,本地因 venv 里已装而没暴露)。

主线的 pyproject 已含本分支需要的全部依赖(onnxruntime<1.24 / qiniu / pillow / numpy,
连注释都是从这条线过去的),故直接取主线版本,两边并集自然成立。uv lock --check 通过。

* fix(providers): 视频下载不再把 API key 带给成品域名

机器审 PR 1024XEngineer#179 P1。成品 URL 是网关响应里的绝对地址(正常指向 CDN,异常可以是
网关返回的任意地址),原实现复用带 Authorization 的网关 client 直接 GET。httpx
只在跨源**重定向**时才自动摘 Authorization,对一开始就跨源的直连请求会原样带上
client 级 headers —— API key 因此发给了那个域名。

改法:
- 按目标地址判定后显式摘凭证,不是一律摘。网关也可能签发自己域名下的下载链接,
  那条路径摘了头就是 401,所以同源保留、跨源摘掉 Authorization 与 Cookie。
- Proxy-Authorization 不动:它是给代理的,与目标是否同源无关。
- 同源判据对齐 httpx 自己的 `_redirect_headers`(scheme + host + 端口),
  未 import 其私有函数,免得被上游改名。
- 请求改为进重试循环之前构造,非 http(s) 地址在发出任何一次请求之前就炸。
- 2026-08-05 实测挣来的三次退避重试与 Content-Length 校验原样保留(视频已生成、
  费用已产生,断一次不能整单作废),FAL 面调用处那句"用同一个 client 带鉴权头取"
  的注释同步更正 —— 它正是这个泄漏的出处。

变异验证 13 个:12 被杀。唯一存活的是单独拆掉"默认端口补齐" —— httpx 0.28 已把
:443/:80 归一化成 port=None,该行与 scheme 比较互为冗余,两条同时拆即被杀。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(providers): 实现文生图 provider,端点不再必然失败

POST /generation/image 是可达端点,ImageTaskExecutor 默认实例化 SufyImageProvider,
而该类的 gen_image 直接抛 NotImplementedError —— 每个图像任务都稳定走到 FAILED。
端点看着可用、实际必失败,是本仓最忌讳的形态(机器审逮到)。

实现要点:
- 走 OpenAI 兼容的 /chat/completions 面,参考图以 data URI 塞进 content 数组。
  与 i2v 的提交-轮询-下载三段式是完全不同的调用形状,不复用 VideoProvider 通路。
- 对整个响应 JSON 正则取 data URI,不猜 message.content 的层级:不同网关包裹层级
  不一致,猜错的代价是"调用成功、费用已产生、但我们报没图"。
- 空图重试 3 次。模型偶发返回一条不含图的正常响应;这与 _download 的网络重试是两
  码事,后者治连接断。
- 校验 base64 解出的字节数下限 5000。响应里可能带几十字节的占位串,当图存下去就是
  一个打不开的文件。
- 取不到有效图抛 RuntimeError,不返回空 bytes:上游会把返回值直接上传对象存储并写
  进任务结果,0 字节的"成功"就是用户看到的裂图。

通路取自已跑通的实现(同日用它出过三张角色母版),非新写。

测试 5 条,全 mock 无付费调用,逐条做过变异测试:
把重试改成 1 次 / 去掉字节下限校验 / 丢掉参考图 / 拿不到图返回空 bytes / 成功后不
早退,五个变异各让 1~3 条用例变红。

* fix(providers): 文生图走配置里的路径,并把"网关没这个模型"翻译成能照着修的错误

对抗复查自己今天这笔实现时发现的两处:

一、路径此前硬编码 "/chat/completions",而 AIProviderSettings.chat_completions_path
   本来就在配置里、零消费方 —— 正是本轮在删的那类字段。改成读配置。

二、更要紧:同一把 key 下不同网关的模型目录**不一样**。实测 GET /v1/models:
   一个网关 73 个模型、一个图像模型都没有;另一个 134 个、含本模块的默认模型
   (2026-08-10 实测)。配错 AI_BASE_URL 时原始报错只是一条裸 404,读的人无从判断
   该改配置还是改模型名。现在 400/404 一律翻译成指向 GET {base}/models 的错误。

这条修的是"错误信息不可操作",不是"配置错误本身"——后者要在部署侧确认网关目录里
确实有所用模型,代码管不了。

测试 +3(路径来自配置、400/404 给出目录提示)。变异测试:路径写死 1 条红、去掉错误
翻译 2 条红。

* fix(providers): 抠图补封闭空洞,但腿间空隙必须按颜色豁免

放大看交付帧,主体内部有透明洞,背景直接透出来。2026-08-11 在归档角色
「林间斥候」走路的 121 帧真实视频帧(1280×720)上把成因拆开量了一遍:

- u2netp 自己在主体内部造的洞:8 帧抽样里 6 帧为 0 —— 不是主要成因;
- 真正的成因是键控误杀:_flat_bg_penalty 每帧杀掉 820~2346 个 u2netp 判为
  主体的像素。角色浅肤色 (243,221,200) 到母版灰底 (219,219,220) 的欧氏距离
  只有 31.3,窄于 _KEY_KILL=38,于是大腿、小臂这些浅色皮肤被当底色抠掉。
  这些被误杀的像素被主体围住,就是「封闭空洞」,填回去即修复。

**只按「不与画面边界连通」判定会把两腿之间填实。** 直觉上腿间空隙从下方通到
画幅底边所以天然安全,实测不成立:迈步相里两只靴子在下方交叠,把空隙彻底封死。
121 帧里 80 帧存在这种封闭的底色空隙,共 25173 像素;只判连通性的朴素版把这
25173 像素**全部**填成主体(最惨单帧 src_024 填掉 3172 像素,两条腿焊在一起,
截图见验证记录)。归档的 04_走路_原画帧/frame_03 同样有 129 像素的封闭腿间空隙。

所以判据是连通性 + 颜色两条一起:一个透明连通域只要「碰到画幅边界」或者
「里面存在任何一个确实是底色的像素」,就不是洞。两条否决合成一次扩散,种子 =
边界上的透明像素 ∪ 底色像素。实测结果:

- 25173 个真空隙像素,守卫版填掉 0 个,朴素版填掉 25173 个;
- 121 帧合计填回 51273 个被误杀的主体像素(朴素版 82118,多出来的就是空隙);
- alpha 只增不减,改动值只能是 1.0,RGB 通道不碰 —— 没有洞的帧逐像素不变。

_HOLE_BG_TOL=14 的取值有实测依据:视频帧里纯背景区域的色距 p99.9≈6.5、
最大 11.1(压缩噪点),而被误杀的浅肤色连通域中位色距 ≥17.1,14 落在这条间隙里。

扩散不用逐像素 BFS:1280×720 约 92 万像素,纯 Python BFS 要几十秒,抠图是逐帧
调用的扛不住。改成按行/列游程传播,一个 pass 推过整条游程。实测填洞单独耗时
34ms,cutout 端到端 0.44s/帧。scipy 不在依赖里,没有为此新增依赖。

顺手把四角估底色抽成 _bg_key(),让「底色是什么」只有一个真相源 —— 键控清理和
填洞必须按同一个 key 判,否则一个把某块当背景清掉、另一个又把它当主体填回来。

变异测试(9 个变异逐个改坏实现 → 确认对应用例变红 → 还原,全部被杀):
  M1 去掉颜色守卫(种子只剩边界,即朴素设计)→ closed_leg_gap 红
  M2 去掉边界种子                              → border_touching 红
  M3/M4 _spread 只做行传播 / 只做列传播        → spread_is_four_connected 红
  M5 去掉「底不是纯色就停手」的早退            → non_flat_background 红
  M6 填成 0.5 而不是 1.0                       → enclosed_hole_is_filled 红
  M7 _HOLE_BG_TOL 放大到 200                   → enclosed_hole_is_filled 红
  M8 _HOLE_BG_TOL 归零                         → closed_leg_gap 红
  M9 丢掉「封闭」条件                          → closed_leg_gap 等 4 条红
另外 _spread 与逐像素 BFS 在 300 组随机掩码 + 螺旋形上逐点等价(用例里留了 25 组)。

CI: ruff / lint-imports(2 contracts kept) / pytest 185 passed 全过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(providers): 模型型号进配置,请求形状留在代码里

人工评审指出 providers 层硬编码过多。拆开看是三类,处理方式不同:

**改进配置**(本次做的):三条能力各自的模型型号。
`AIProviderSettings` 加 `video_model` / `image_model` / `fal_video_model`,默认值即当前
实测在用的型号,部署侧可用 AI_VIDEO_MODEL / AI_IMAGE_MODEL / AI_FAL_VIDEO_MODEL 覆盖。
分成三个字段而不是共用已有的 `model`:三条能力同时在用不同模型,共用一个意味着换其中
一条把另外两条也换了。显式传参仍优先于配置,方便 A/B 对比时不必改环境变量。

**留在代码里**(本次不做,理由写进配置类的注释):哪个模型吃 image_list、哪个吃
input_reference、FAL 队列路径长什么样。这些不是运行参数,是该模型的 API 形状事实,改变
的是请求怎么构造。放进配置会把"填错了会怎样"从部署期推到运行期 —— 字段塞错不会立刻
报错,任务照常 queued,直到生成阶段才 failed,而费用可能已经产生(2026-07-29 实测)。

**暂不处理**:重试次数与字节下限。可配置化,但现在提出去只增加配置面,等真要调再说。

顺带修一个这批测试逮到的真 bug:`FalQueueVideoProvider` 的构造期校验发生在型号解析
**之前**,于是 `model=None`(表示"用配置里的")会被直接拿去查端点表,报"模型 None 不在
表里"—— 走默认路径就构造失败。改成先解析型号再校验。

测试 +5,5 条变异全部杀掉(共用一个字段 / 忽略配置写回硬编码 / 显式传参被配置覆盖 /
配置里补上请求形状字段 / 校验挪回解析之前)。

* refactor(providers): 移除从未真实调用过的 FAL 队列面

评审质疑「为什么还需要一层不该理解业务的东西」(指 FirstFrameUploader)。查证后我认同,
但比他说的更彻底:**整个 FAL 队列面从未被真实调用过** —— app / ai_engine 里零引用,
产品链路走不到它,唯一的"引用"是 interfaces.py 里一句 docstring 指路。

删掉的理由与 GenRoute 只列有实现的路线是同一条,也是我在这批 PR 里反复引用的判据:
没有消费方的代码等于死代码,它让调用方以为该能力已具备。我一边用这条原则删掉
ActionSpec.fps / loop、一边留着 412 行未验证的 provider,是自相矛盾的。

删除:FalQueueVideoProvider / FirstFrameUploader / PreUploadedFirstFrame /
FAL_I2V_ENDPOINTS 与端点映射 / 三个 FAL 专用异常 / config.fal_video_model /
28 条 FAL 测试。sufy.py 从 740 行降到 343 行。

保留一段注释记下两个实测挣来的事实,避免将来重新摸索:FAL 面只吃公网 URL 不吃 base64
(塞 base64 会 queued 之后在生成阶段才 failed,费用可能已产生);鉴权头是
`Authorization: Key`,路径与 /v1 平级。

顺带把 VideoProvider 的 docstring 改成正面依据:**入参恒为 bytes**,因为 ai_engine 必须
持有 bytes —— master_check 预检、master_prep 预处理、像素化锁色板全都读母版像素;改传
URL 的话 ai_engine 还得自己下载回来。某厂商只吃 URL 属该 provider 自己的适配问题,
在 provider 内部转换,不把差异漏给上层。

代价如实说明:veo / seedance 只在 FAL 面,而实测 veo 的走路步态比 kling 更自然。真要接
时连同一次真实调用一起加回,归档里有完整的接入记录,重写成本不高。

* test(providers): 补 i2v 主流程与 cutout 装配的覆盖,并修一个除零

CI 的 codecov/patch 报红,查证后是真缺口:`SufyVideoProvider.i2v` —— **产品唯一的付费
路径** —— 一条测试都没有。sufy.py 覆盖率 72%,未覆盖的正是提交/轮询/下载三段式与首帧
处理。matte.py 的 `cutout` 装配顺序同样零覆盖。

补 sufy 7 条(sufy.py 72% → 99%):
- 完整付费路径:提交拿 job id → 轮询到 completed → 下载 mp4
- 首帧必须是 JPEG data URI。PNG base64 会让任务 status=failed(VENDOR_FAILED,
  2026-07-22 实测,33s fail-fast)—— 这条错在提交之后才报,本地看不出来
- 首帧按目标画布**补边不拉伸**:拉伸会改角色比例,而母版比例是角色一致性的一部分
- failed / cancelled 立刻抛,不把剩余轮询预算耗完(钱已经花了,尽快暴露原因更有用)
- 轮询预算用尽抛错而不返回空 bytes(空 bytes 会被当视频送进抽帧,报"无可解码帧",
  真正的原因被埋掉)
- 首帧字段按模型选(塞错字段任务照常 queued,直到生成阶段才 failed,费用可能已产生)

补 matte 3 条(matte.py 71% → 93%):cutout 输出 RGBA、**RGB 通道不被改动**(改了会让
后续像素化锁色板取到被改过的颜色)、清理与填洞的**调用顺序**(反过来会把刚填上的像素
又清掉,且不报错)。真实推理需要 4.7MB onnx 权重,CI 里下不到也不该下,故用假 session
只覆盖装配逻辑。

顺带修一个测试逮到的真 bug:`poll_interval=0` 会在 `max_min * 60 // poll` 处除零,报
ZeroDivisionError,读的人完全看不出是配错了参数。改为构造期拒绝非正数。

9 条变异全部杀掉。其中"补边不拉伸"第一版是摆设 —— 纯色图拉伸后对称两点颜色照样相同,
M3 存活;改成在源图里放一个偏心方块、量它在成品里的宽高比(补边≈1.0,拉伸≈2.67)
才真能杀掉。

另记一个操作教训:变异测试期间用 `git checkout -- <file>` 还原,会把同文件里**尚未提交**
的改动一起丢掉(守卫被静默还原,表现为"还原后测试仍红")。变异 harness 一律用脚本内的
文本备份还原,并在结束时校验 sha256。

---------

Co-authored-by: johnnyzhang-eng <johnnyzhang-eng@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(playtest): add asset selection entry

PlayTest had no stable navigation target without a selected character and outfit.

Add protected asset selection, header and character-detail entry points, and the animated dot-matrix stage.

Users can now discover PlayTest and choose a playable outfit before entering the workbench.

* test(playtest): cover entry navigation

The new PlayTest surfaces need regression coverage across authentication, navigation, and asset states.

Cover header activation, guest return paths, character-detail links, entry states, and animation timing.

The tests protect the complete selection flow without relying on Canvas rendering in jsdom.

* test(playtest): exercise pixel stage rendering

Codecov reported the new canvas implementation as uncovered because the existing test stopped at a null rendering context.

Run representative animation frames against a focused canvas double and verify the continuous scheduling and cleanup lifecycle.

The Playtest entry and pixel stage now reach full line coverage in the targeted coverage run.
…Engineer#95)

* refactor(quick-start): align animation review workflow

* test(quick-start): cover review and publish flow

* test(quick-start): cover recovery branches

* test(quick-start): cover candidate lifecycle

* fix(quick-start): wire authenticated production flow

* fix(quick-start): defer API base URL resolution

* test(quick-start): cover recovery and publish failures

* test(quick-start): cover workflow polling fallback

* test(quick-start): verify authenticated generation transport

* refactor(quick-start): bind service to run session

* test(quick-start): cover session lifecycle

* fix(quick-start): make review publishing retryable

* test(quick-start): cover recovery controls
…Engineer#215)

* refactor(quick-start): align animation review workflow

* test(quick-start): cover review and publish flow

* test(quick-start): cover recovery branches

* test(quick-start): cover candidate lifecycle

* fix(quick-start): wire authenticated production flow

* fix(quick-start): defer API base URL resolution

* test(quick-start): cover recovery and publish failures

* test(quick-start): cover workflow polling fallback

* test(quick-start): verify authenticated generation transport

* refactor(quick-start): bind service to run session

* test(quick-start): cover session lifecycle

* fix(quick-start): make review publishing retryable

* test(quick-start): cover recovery controls
Honor reduced-motion overrides for active project marks.

React to preference changes without remounting the home canvas.

Add focused coverage for stopping scheduled animation frames.
* feat(workflow-editor): add reference image upload

Workflow runs could store media references but the editor had no way to create them.

Route character reference files through the shared media adapter and persist the returned reference on the setup node.

Users can attach an image before generating character candidates and see upload failures in place.

* test(workflow-editor): cover reference image upload

The new editor upload path needs focused coverage at its runtime and page boundaries.

Exercise media category forwarding, successful persistence, upload locking, and failure handling.

The targeted workflow editor suite now guards the reference image flow without broadening the gate.

* fix(workflow-editor): cancel uploads on unmount

In-flight reference uploads outlived the editor session during navigation.

Thread an AbortSignal through the session boundary and stop late completions before controller persistence.

Leaving the editor now cancels the request without surfacing stale errors or orphaning its result.

* test(workflow-editor): cover upload cancellation

Navigation could leave a reference upload unresolved after the editor session was disposed.

Exercise signal forwarding, unmount cancellation, and rejection of a late upload completion.

The regression test protects the lifecycle boundary without widening the suite.
* fix(quick-start): find characters across pages

Quick Start could miss a run-bound character beyond the first page.

Traverse character pages while preserving the single-match constraint.

Final approval can resolve later-page character bindings.

* test(quick-start): cover later-page character binding

Quick Start lacked coverage for bindings beyond the first character page.

Model a project whose run-bound character appears on page two.

Protect character resolution from first-page-only regressions.
* fix(quick-start): fill the viewport canvas

Tall desktop viewports exposed the white application shell below the Quick Start surface.

Use viewport-height canvases and remove the unintended outer corner radius in entry and run states.

The creation surface now reaches every viewport edge while keeping inner component styling intact.

* test(quick-start): cover viewport-height canvases

The fixed-height regression affected both Quick Start entry and active run surfaces.

Render both routes and assert that their top-level canvases use the viewport-height contract.

Future layout changes will catch a return to the truncated canvas behavior.
…1024XEngineer#180)

* feat(framework): 补 provider 抽象接口与抠图/视频实现

providers/ 此前只有三个 create_*_client 工厂,没有可供上层依赖的抽象类型,
ai_engine 无法在不 import 具体实现的前提下声明它需要什么能力。

- interfaces.py:ImageProvider / VideoProvider / MatteProvider 三个 Protocol,
  零依赖,供上层按能力而非按厂商声明依赖。
- matte.py:OnnxU2NetMatteProvider,onnxruntime 直跑 u2netp。不用 rembg:其底层
  同样依赖 onnxruntime,且 numba 老链在 3.12 无轮子。onnxruntime 导入失败时降级
  到 Pillow 兜底而非崩溃。
- sufy.py:SufyImageProvider / SufyVideoProvider。视频成品下载加三次退避重试与
  长度校验 —— 该步发生在提交任务、轮询、等待全部成功之后,此时费用已产生、视频
  已生成好,只差取回数据,连接断一次整单作废。实测同一角色连续两单死在这里各烧
  一次费用。test_sufy_video_download 的四条断言拿修复前的旧实现做过对照,确认其中
  三条在修复前会失败。

依赖声明:
- qiniu>=7.14 —— 此前未声明,镜像能起、/docs 也 200,只有第一次 POST /media/upload
  才 ModuleNotFoundError。
- onnxruntime>=1.17,<1.24 —— 1.24 起不再发布 macOS Intel(x86_64) wheel,Intel Mac
  装不上。1.23.x 仍覆盖 Intel/arm64/Linux + py3.12,API 一致,抠图代码零改动。

本 PR 不依赖其他未合分支:providers 不 import windup_common.models。

* feat(providers): 按现行 FAL 队列接口重写 i2v,并回退按旧接口形状打的两处补丁

2026-08-07 拉网关 OpenAPI spec 逐个核对:平台现有 69 个 POST 视频端点,其中 22 个
图生视频**全部**在 FAL 队列面 /queue/... 下,首帧一律是 URL 形态字段(image_url /
start_image_url),同日实测送 base64 dataURI 无一能用。原 SufyVideoProvider 建在
OpenAI 风格 /v1/videos + input_reference dataURI 上,是过时的接口形状——在它上面打的
两处补丁方向错了,一并回退:

- _needs_image_list / _IMAGE_LIST_MODELS 里新增的 kling-v3-omni / kling-v3
- _assert_reference_registered / ReferenceIgnoredError 及其 3 条测试

新增 FalQueueVideoProvider 与旧实现并存(没有实测证据说 /v1/videos 已坏,sora 系可能
仍只在那一面)。要点:

1) 模型 → 端点的显式硬表 FAL_I2V_ENDPOINTS,不拼路径。每家有三样东西不同且都猜不出
   来:提交路径的型号段;首帧字段名(同是 kling,o3 / v2.5-turbo 叫 image_url,
   v3 / v2.6 / o1 叫 start_image_url);轮询前缀(**不是**提交路径 + /requests,
   kling 六个型号共用 /queue/fal-ai/kling-video/requests/{id})。未登记的模型抛
   UnknownVideoModelError,不做前缀匹配、不做兜底——猜出一条"存在但语义不同"的路径
   (如把 image-to-video 猜成 reference-to-video)会正常出片、正常计费。

2) i2v 契约冲突:Protocol 收 bytes,FAL 面只吃公网 URL。选择"provider 自己适配",
   Protocol 签名不动——新增 FirstFrameUploader port,provider 构造时必传,内部把补边
   后的首帧换成 URL。调用方零改动;母版已在公网时用 PreUploadedFirstFrame 复用该
   URL、不重传。

3) 失败一律显式抛错,不静默降级:spec 明写「任务失败时后端也返回 COMPLETED,通过
   detail 区分」,故 COMPLETED 还要查 detail;认不出的 status 当失败(继续轮询会把
   "协议变了"伪装成"生成太慢");超时抛 VideoJobTimeoutError;参数校验在上传首帧之前
   完成;下载复用既有 _download(重试 + 长度校验,治"视频已生成、费用已产生,下载断
   一次整单作废")。

FAL 面鉴权是 Authorization: Key(不是 Bearer),base_url 需从 /v1 退回网关根
(/queue 与 /v1 平级)。两处都有 spec 依据,已写进注释与测试。

37 条新测试全程 mock 不联网;11 个变异(错端点 / 错字段名 / 错轮询前缀 / 去掉各处抛错
/ 去掉下载重试 / 参数校验挪到上传后)逐个确认能被测到,全部 KILLED。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(framework): 依赖声明取主线版本,修 rebase 时 lock 与 pyproject 不一致

rebase 到 main 时解冲突取了主线的 uv.lock,但 framework/pyproject.toml 取了本分支的,
后者少了主线用户模块加的 passlib[bcrypt] / redis / resend —— CI 装依赖时按 pyproject
解析,于是 conftest.py 导入 bcrypt 失败(ModuleNotFoundError,本地因 venv 里已装而没暴露)。

主线的 pyproject 已含本分支需要的全部依赖(onnxruntime<1.24 / qiniu / pillow / numpy,
连注释都是从这条线过去的),故直接取主线版本,两边并集自然成立。uv lock --check 通过。

* fix(providers): 视频下载不再把 API key 带给成品域名

机器审 PR 1024XEngineer#179 P1。成品 URL 是网关响应里的绝对地址(正常指向 CDN,异常可以是
网关返回的任意地址),原实现复用带 Authorization 的网关 client 直接 GET。httpx
只在跨源**重定向**时才自动摘 Authorization,对一开始就跨源的直连请求会原样带上
client 级 headers —— API key 因此发给了那个域名。

改法:
- 按目标地址判定后显式摘凭证,不是一律摘。网关也可能签发自己域名下的下载链接,
  那条路径摘了头就是 401,所以同源保留、跨源摘掉 Authorization 与 Cookie。
- Proxy-Authorization 不动:它是给代理的,与目标是否同源无关。
- 同源判据对齐 httpx 自己的 `_redirect_headers`(scheme + host + 端口),
  未 import 其私有函数,免得被上游改名。
- 请求改为进重试循环之前构造,非 http(s) 地址在发出任何一次请求之前就炸。
- 2026-08-05 实测挣来的三次退避重试与 Content-Length 校验原样保留(视频已生成、
  费用已产生,断一次不能整单作废),FAL 面调用处那句"用同一个 client 带鉴权头取"
  的注释同步更正 —— 它正是这个泄漏的出处。

变异验证 13 个:12 被杀。唯一存活的是单独拆掉"默认端口补齐" —— httpx 0.28 已把
:443/:80 归一化成 port=None,该行与 scheme 比较互为冗余,两条同时拆即被杀。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(ai_engine): 出帧工具箱——抽帧 / 选帧 / 后处理 / 提示词

视频路线的纯计算层,零 windup 依赖(只用 PIL + numpy),可独立测试。

slicing/  视频 → 帧序列
  extract   解码;loop 循环类动作抽单步态周期;oneshot 一次性动作裁区间;
  quality   帧质量诊断(死帧 / 糊帧判据,只作诊断不进选帧,理由见 loop docstring)

postprocess/  帧 → 交付级序列帧
  pixelate  母版是像素画时吸附母版网格 + 锁母版色板,否则通用量化
  pack      脚线对齐 / sprite sheet / GIF
  rootmotion 逐帧时长(关键帧加长定格,等时长会让动作发飘)

prompt/ + master_prep.py  按动作类型选提示词、按动作预处理母版

两处实测挣得的修复一并带上:

1) 画布横向裁切(postprocess/pack.py)
   align_bottom_center 的三条缩放分支只按高度定标,是"主体是纵向长条"的人形先验。
   横向长条主体按同一系数缩放后宽度超出 cell,被 alpha_composite 以负 dest 静默丢像素,
   PIL 不报错。裁切悬崖 w/h ≈ 1.61;实测狐狸母版 w/h=1.78 丢 27px(鼻尖+尾尖),
   w/h=2.0 只剩 79.9% 内容。加宽度兜底 fill_w=0.96;人形 w/h 0.3–1.1 时该约束恒不生效,
   产物逐像素不变。

2) 步态周期误检(slicing/loop.py)三个坑,四段真 i2v 视频实测
   a. 角色整体平移让 d(p) 单调上升,argmin 滑到搜索窗边界交出假周期。
      实测骷髅走路:不消平移时曲线 40/56 段在上升,只剩 22/42/52 三个浅坑,argmin=22;
      加 _deskew 消平移后整条曲线只剩一个局部极小,正是真周期 56(凹陷深度 2.68)。
   b. 搜索窗上界 n//2 把真周期挡在窗外(待机真周期 62 > pmax 60)。改为 total*0.6。
   c. 谐波:22 接近真周期的一半,半周期闭环 = 末帧接回首帧时左右腿瞬间互换。
      改为在基周期整数倍里按归一化接缝复选,优先最小倍数。
   测不到可信凹陷(prominence < 0.25)时判"无周期",退化成全片均匀取、不硬闭环——
   实测骑士待机只有 31 帧,旧算法曲线单调、argmin 落在搜索窗下界 6 交出边界假值。

实测对照(n=16,接缝 = 末→首差 ÷ 组内相邻差均值,越接近 1 越闭合)
  骷髅走路 3.07→1.96 | 骑士走路 1.29→0.87 | 骑士待机 9.31→1.39 | 骑士奔跑 1.77→0.81
  待机那条最直观:旧算法写出的 GIF 只有 6 帧——16 帧里 10 帧逐像素重复,被 PIL 自动去重。

消融:改善全部来自 _deskew + 谐波复选。追加的"死帧避让 + 冻结裁剪"两个样本无变化、
两个变差(奔跑接缝 0.81→2.00),已回退,quality 只留作诊断。

* test(ai_engine): 覆盖逐帧时长与母版预处理两条主路径

`frame_durations` 参与每一次出参构造,`prepare_master` 参与每一次 jump / attack 生成,
此前两者均无直接覆盖。21 个用例,锁行为不锁具体数值。

frame_durations
- 动作间必须有区分度(idle > walk > run)——等时长会让动作发飘、没有重量感
- 关键帧定格必须真的比邻帧长,且只定格一帧
- 未知动作要有可用兜底,不能返回 0 或抛错(上游动作类型可能先于本模块扩展)
- 越界 key_frame 不炸(帧数由选帧决定,调用方未必对齐)
- hold_ms 小于基准时长时取基准,定格不能反而变快

prepare_master
- jump / attack 必须补顶部空间,否则腾空 / 过顶挥砍会顶出视频画面上沿被裁
  (实测 attack 15/72 帧触顶)
- 其余动作必须**原样**返回同一对象,无谓重编码会引入压缩损失
- 补的边在顶部、原图贴底(贴反了动作会往下出画)
- ratio 越小顶部留白越多;jump 需要的空间多于 attack
- 非法 ratio 抛 ValueError

已做变异测试,五处故意引入的错误全部被捕获:
  抹掉动作间时长区分度        → 2 failed
  关键帧不定格                → 1 failed
  jump/attack 不补顶部空间     → 3 failed
  补边加在底部而非顶部         → 1 failed
  jump 与 attack 用同一 ratio  → 1 failed
还原后 21 passed。不是写完就绿。

* refactor(ai_engine): 收拢选帧与帧质量共用的取样原语

_gray() 与 _SMALL=48 此前在 slicing/loop.py 与 slicing/quality.py 各有一份完整拷贝。
两处必须在同一尺度上看帧,否则算出的差异量不可比;而分叉不会报错、只在数据上体现
——调一边的降采样尺寸,另一边悄悄保持 48,两个模块的指标从此不再可比。

收成 slicing/_frames.py 唯一定义(SMALL / gray)。行为不变。

* fix(deps): 重锁 uv.lock 以覆盖视频抽帧依赖

ai_engine 的 pyproject 声明了 imageio / av(抽帧必需),而 rebase 解冲突时 uv.lock
取的是主线版本,两者不一致:uv lock --check 报 lockfile needs to be updated。
本地 venv 里恰好装过这两个包,故本地测试没暴露;CI 用 --frozen 装依赖时会缺。

重锁时带 UV_DEFAULT_INDEX=阿里云镜像 —— 直接 uv lock 会把全仓 90 处包源改写成
pypi.org,产出两千多行与本次改动无关的 diff(主线 Dockerfile 定的就是这个镜像源)。
重锁后:阿里源 90 处、pypi 0 处,只新增 627 行(两个新包及其依赖树)。

* fix(ai_engine): 选帧入参边界——除零 / 静默少给帧一并堵掉

机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n,
凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。

pick_oneshot:
- n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点),
  swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点
  (动作区间前后不对称,中点落在蓄力段)。
- n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。
- 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。
- 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧),
  改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。
- kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。

pick_cycle:
- n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空,
  k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而
  测不到周期时**静默返回 []** —— 后者更危险。
- 源帧不足改为报错(原本原样返回)。
- n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。

测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。
变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组
(total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* perf(ai_engine): 抽帧改流式,不再把整段视频读进内存

机器审 P2:iio.imread 会把 (T, H, W, C) 整个 materialize 出来,而我们只要其中 8~16 帧。

实测(14 段真实 i2v 视频,进程 RSS 峰值,非 tracemalloc):
- 121 帧 720p,抽 16 帧:488 MiB → 126 MiB,降 74%
- 同一段抽 8 帧:460 MiB → 98 MiB,降 79%
- 抽 150 帧(周期检测用的 extract_all_frames_bytes):858 MiB → 498 MiB,降 42%

最后一条如实说明降幅边界:它保留全部 121 帧,省掉的只是那个完整 ndarray,保留帧本身
该占的内存还在。并发 worker 叠加时这仍是主要占用项。

正确性:改造前后在 14 段真实视频 × n=8/16/150 共 42 组上抽出的帧**逐像素完全相同**。
n=1 取首帧的既有约定保持不变(关键姿势的选择归 pick_oneshot,不由抽帧层猜)。

帧数来源:先读容器元数据(在 14 段真实视频上与实际帧数全部一致),拿不到正整数就退回
逐帧计数——计数不保留帧、内存不涨。按错的帧数算下标会抽出错位的帧,那是"帧数对、内容
错"的静默失败,多解一遍换一个确定的数划算。

顺带:imageio 分支的 except Exception 加了 warning 日志。它此前把"我们自己算错下标"和
"环境里没装 imageio"混为一谈,两者都表现为悄悄换用 ffmpeg 分支、产出看着正常的帧。

测试 11 条(tests/test_extract_streaming.py),含下标边界、抽到的是首尾与均匀分布那几帧、
要的比有的多时不补帧、元数据报 0 或抛错时退回计数。做过变异测试:改回 imread 让两条变红。
其中一条最初是摆设——炸弹抛 AssertionError 被 except Exception 吞掉、静默走 ffmpeg 后照
样绿,改成 BaseException 子类才真能杀掉变异;docstring 里写明了这个坑。

* test(ai_engine): 提示词构造器的实现侧断言补在引入它的分片

契约断言(DTO 自身)留在 feat/character-domain-models,不 import 上层包;本分片引入
prompt 模块,配套的实现侧断言就该落在这里:类型注解不是运行期约束,把校验写成
`SIDE if facing == Facing.SIDE else FRONT` 的二分时,"sidee" 会静默落到 FRONT 模板——
正面走的提示词配侧面母版,模型靠转身调和矛盾,调用方什么错都收不到。

4 条:四个 build_* 拒绝非法 facing、枚举与合法字符串等价、walk 按 facing 选对模板体、
其余 build_* 同样按 facing 切换。

* feat(ai_engine): 交付画布支持非方形,引擎可一次出到项目 sprite 尺寸

交付帧一直是 256×256 方形,而项目的 sprite 尺寸是 sprite_width×sprite_height
(API 允许 32~2048,且宽高各自独立、可非方)。上层拿到 256 的帧再缩到项目尺寸,
问题不是"糊一点":那一步用 Image.thumbnail,而 **thumbnail 只缩不放**。
2026-08-11 实测复刻上层这段逻辑,喂一张主体高 157px、脚线 0.92 的 256 交付帧:

    目标 512×512 → 画布 512,主体仍 157px(根本没放大),脚线 0.92 → 0.709
    目标 384×384 → 画布 384,主体仍 157px,                脚线 0.92 → 0.779
    目标 128×128 → 画布 128,主体 78px,                  脚线 0.914(缩小这侧正常)

也就是说放大方向上,align_bottom_center 刚对齐好的脚线被整体挪高,角色不站在地上,
跨动作对齐(ref_height 那套)也一起失效。根治办法是引擎一次就出到目标尺寸。

align_bottom_center 本来就接受 cell,按 cell 出 512 时主体高度实测 154 → 308,
确实翻倍;缺的只是**非方形**能力:cell 只能出方形,非方 sprite 仍得回到上层补边。
本次加 cell_h(None = 方形 cell×cell,默认行为不变),并把体内的几何拆成
cw(宽:水平居中、宽度兜底)与 ch(高:脚线、占高定标),不许串轴。

实测(同一组帧,ref_height=300):
    默认           canvas 256×256  主体高 159  脚线 0.918  水平中心 0.498
    cell=512       canvas 512×512  主体高 317  脚线 0.920  水平中心 0.500
    cell=384,h=512 canvas 384×512  主体高 317  脚线 0.920  水平中心 0.500
    cell=128,h=192 canvas 128×192  主体高 119  脚线 0.917  水平中心 0.496

**默认行为逐像素不变**:default / ref_height / preserve_lift / 宽主体兜底 /
cell=128 / cell=512 六个用例改动前后 sha256 完全一致;另有用例钉死
"不传 cell_h" 与 "cell_h=cell" 两种写法逐像素相同。

几何用比例表达(foot_line / fill_h / fill_w),换画布尺寸不改变构图,所以母版入口
预检与出帧仍共用同一套几何 —— master_check.REJECT_ASPECT = 2*FILL_W/FILL_H 里
本来就没有 cell,与画布像素尺寸无关。用例
test_subject_fill_ratio_is_scale_invariant 在 128/256/512/1024 四档上钉死这条。

顺带:cell/cell_h 非正数改为报错。PIL 允许建 0×0 的图、alpha_composite 也不报错,
静默出一张空图要到落库或前端才暴露。

变异测试(8 个变异逐个改坏 → 确认变红 → 还原,全部被杀):
  P1 cell_h 默认写死 256          → doubling_cell_doubles_subject_height 红
  P2 主体占高改按画布宽算          → non_square_applies_each_axis 红
  P3 脚线改按画布宽算              → non_square_applies_each_axis 红
  P4 水平居中改按画布高算          → non_square_applies_each_axis 红
  P5 宽度兜底改按画布高算          → width_fallback_uses_canvas_width 红
  P6 去掉画布尺寸校验              → non_positive_canvas_raises 红
  P7 全透明兜底退回 256 方形       → all_transparent_honour_canvas 红
  P8 出帧画布忽略请求值            → 另外 3 条红(默认档下该变异是恒等,测不到属正常)

本提交只动 ai_engine;把尺寸从 app 传进引擎的接线在上层分支。
CI: ruff / lint-imports(2 contracts kept) / pytest 255 passed 全过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(tests): 删掉 rebase 带回来的 FAL 测试文件

FAL 队列面已随 1024XEngineer#179 移除,这个测试文件也一并删了。rebase 到新 main 时它被重放回来,
而它引用的 8 个 FAL 符号已不存在 —— 收集期直接 ImportError。

---------

Co-authored-by: johnnyzhang-eng <johnnyzhang-eng@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(landing): add gongbi bird illustrations

Windup needs a recognizable visual extension of its bird mark.

Add three transparent Gongbi bird cutouts with semantic filenames.

Two birds serve the hero while the perched variant remains available for future use.

* feat(landing): add product media

Landing sections need real product and character imagery.

Add the Workflow Editor capture and character journey artwork.

The page can present actual media without fabricated interface assets.

* feat(landing): add motion preference utilities

Landing motion must respect reduced-motion environments.

Add a guarded shared hook for the system preference.

Components can respond without assuming matchMedia is available.

* feat(landing): add capability rail

The product story needs a concise capability overview.

Add the horizontal capability rail and isolated progress model.

Scrolling maps to the rail while reduced motion keeps content accessible.

* feat(landing): add public product page

Windup needs a public product narrative ahead of the workspace.

Add the editorial landing page, marketing header, and scoped visual system.

Visitors can understand the workflow before choosing to enter the product.

* feat(landing): add page metadata

The public page needs a concise search and sharing description.

Add a product-focused meta description to the frontend document.

Browser and crawler context now matches the landing proposition.

* test(landing): cover public entry behavior

The new public page introduces authentication and anchor paths.

Cover guest and authenticated CTAs, real media, and scroll progress.

Regressions in landing navigation or asset use fail locally.

* feat(layout): add public marketing shell

The landing page must not inherit protected product navigation.

Add a public shell that retains account and session overlays.

Marketing and workspace pages now have explicit layout ownership.

* feat(routes): place landing before the workspace

The root route now represents the public product entry.

Move the existing home into /workspace and align product header links.

Authenticated users remain on the landing page until they enter the protected workspace.

* test(routes): cover landing workspace boundary

The route split changes guest and authenticated navigation.

Cover root, protected workspace, and lazy workflow shell behavior.

Route ownership and login return paths remain explicit.

* test(workspace): align workspace navigation coverage

The former home page now lives under the workspace route.

Rename its test subject and assert product header workspace links.

Existing workspace entry behavior remains covered after the route move.

* docs(landing): add primary page screenshots

Reviewers need runtime evidence for the visible landing change.

Add current desktop captures for the hero and capability story.

The PR can show the implemented page instead of design mockups.

* docs(landing): add section screenshots

Lower landing sections also need reviewable runtime evidence.

Add desktop captures for styles, pipeline, and workspace hierarchy.

Reviewers can verify each major page section from the PR.

* perf(landing): optimize marketing assets

Landing illustrations were stored as large PNG files and runtime images used mixed public and imported paths.

Convert the four illustrations to high-quality WebP and move all landing media into dependency-managed asset folders.

Reduce shipped image weight while preserving transparent edges and hashed build output.

* refactor(landing): migrate styles to Tailwind

Landing-specific selectors were embedded in the global stylesheet and obscured the page boundary.

Move static presentation into component Tailwind classes while keeping only page-level keyframes and scroll behavior in a private stylesheet.

Preserve desktop rendering and reduced-motion behavior while returning index.css to shared tokens and global rules.
…Engineer#53) (1024XEngineer#181)

* feat(framework): 补 provider 抽象接口与抠图/视频实现

providers/ 此前只有三个 create_*_client 工厂,没有可供上层依赖的抽象类型,
ai_engine 无法在不 import 具体实现的前提下声明它需要什么能力。

- interfaces.py:ImageProvider / VideoProvider / MatteProvider 三个 Protocol,
  零依赖,供上层按能力而非按厂商声明依赖。
- matte.py:OnnxU2NetMatteProvider,onnxruntime 直跑 u2netp。不用 rembg:其底层
  同样依赖 onnxruntime,且 numba 老链在 3.12 无轮子。onnxruntime 导入失败时降级
  到 Pillow 兜底而非崩溃。
- sufy.py:SufyImageProvider / SufyVideoProvider。视频成品下载加三次退避重试与
  长度校验 —— 该步发生在提交任务、轮询、等待全部成功之后,此时费用已产生、视频
  已生成好,只差取回数据,连接断一次整单作废。实测同一角色连续两单死在这里各烧
  一次费用。test_sufy_video_download 的四条断言拿修复前的旧实现做过对照,确认其中
  三条在修复前会失败。

依赖声明:
- qiniu>=7.14 —— 此前未声明,镜像能起、/docs 也 200,只有第一次 POST /media/upload
  才 ModuleNotFoundError。
- onnxruntime>=1.17,<1.24 —— 1.24 起不再发布 macOS Intel(x86_64) wheel,Intel Mac
  装不上。1.23.x 仍覆盖 Intel/arm64/Linux + py3.12,API 一致,抠图代码零改动。

本 PR 不依赖其他未合分支:providers 不 import windup_common.models。

* feat(providers): 按现行 FAL 队列接口重写 i2v,并回退按旧接口形状打的两处补丁

2026-08-07 拉网关 OpenAPI spec 逐个核对:平台现有 69 个 POST 视频端点,其中 22 个
图生视频**全部**在 FAL 队列面 /queue/... 下,首帧一律是 URL 形态字段(image_url /
start_image_url),同日实测送 base64 dataURI 无一能用。原 SufyVideoProvider 建在
OpenAI 风格 /v1/videos + input_reference dataURI 上,是过时的接口形状——在它上面打的
两处补丁方向错了,一并回退:

- _needs_image_list / _IMAGE_LIST_MODELS 里新增的 kling-v3-omni / kling-v3
- _assert_reference_registered / ReferenceIgnoredError 及其 3 条测试

新增 FalQueueVideoProvider 与旧实现并存(没有实测证据说 /v1/videos 已坏,sora 系可能
仍只在那一面)。要点:

1) 模型 → 端点的显式硬表 FAL_I2V_ENDPOINTS,不拼路径。每家有三样东西不同且都猜不出
   来:提交路径的型号段;首帧字段名(同是 kling,o3 / v2.5-turbo 叫 image_url,
   v3 / v2.6 / o1 叫 start_image_url);轮询前缀(**不是**提交路径 + /requests,
   kling 六个型号共用 /queue/fal-ai/kling-video/requests/{id})。未登记的模型抛
   UnknownVideoModelError,不做前缀匹配、不做兜底——猜出一条"存在但语义不同"的路径
   (如把 image-to-video 猜成 reference-to-video)会正常出片、正常计费。

2) i2v 契约冲突:Protocol 收 bytes,FAL 面只吃公网 URL。选择"provider 自己适配",
   Protocol 签名不动——新增 FirstFrameUploader port,provider 构造时必传,内部把补边
   后的首帧换成 URL。调用方零改动;母版已在公网时用 PreUploadedFirstFrame 复用该
   URL、不重传。

3) 失败一律显式抛错,不静默降级:spec 明写「任务失败时后端也返回 COMPLETED,通过
   detail 区分」,故 COMPLETED 还要查 detail;认不出的 status 当失败(继续轮询会把
   "协议变了"伪装成"生成太慢");超时抛 VideoJobTimeoutError;参数校验在上传首帧之前
   完成;下载复用既有 _download(重试 + 长度校验,治"视频已生成、费用已产生,下载断
   一次整单作废")。

FAL 面鉴权是 Authorization: Key(不是 Bearer),base_url 需从 /v1 退回网关根
(/queue 与 /v1 平级)。两处都有 spec 依据,已写进注释与测试。

37 条新测试全程 mock 不联网;11 个变异(错端点 / 错字段名 / 错轮询前缀 / 去掉各处抛错
/ 去掉下载重试 / 参数校验挪到上传后)逐个确认能被测到,全部 KILLED。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(providers): 视频下载不再把 API key 带给成品域名

机器审 PR 1024XEngineer#179 P1。成品 URL 是网关响应里的绝对地址(正常指向 CDN,异常可以是
网关返回的任意地址),原实现复用带 Authorization 的网关 client 直接 GET。httpx
只在跨源**重定向**时才自动摘 Authorization,对一开始就跨源的直连请求会原样带上
client 级 headers —— API key 因此发给了那个域名。

改法:
- 按目标地址判定后显式摘凭证,不是一律摘。网关也可能签发自己域名下的下载链接,
  那条路径摘了头就是 401,所以同源保留、跨源摘掉 Authorization 与 Cookie。
- Proxy-Authorization 不动:它是给代理的,与目标是否同源无关。
- 同源判据对齐 httpx 自己的 `_redirect_headers`(scheme + host + 端口),
  未 import 其私有函数,免得被上游改名。
- 请求改为进重试循环之前构造,非 http(s) 地址在发出任何一次请求之前就炸。
- 2026-08-05 实测挣来的三次退避重试与 Content-Length 校验原样保留(视频已生成、
  费用已产生,断一次不能整单作废),FAL 面调用处那句"用同一个 client 带鉴权头取"
  的注释同步更正 —— 它正是这个泄漏的出处。

变异验证 13 个:12 被杀。唯一存活的是单独拆掉"默认端口补齐" —— httpx 0.28 已把
:443/:80 归一化成 port=None,该行与 scheme 比较互为冗余,两条同时拆即被杀。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(ai_engine): 出帧工具箱——抽帧 / 选帧 / 后处理 / 提示词

视频路线的纯计算层,零 windup 依赖(只用 PIL + numpy),可独立测试。

slicing/  视频 → 帧序列
  extract   解码;loop 循环类动作抽单步态周期;oneshot 一次性动作裁区间;
  quality   帧质量诊断(死帧 / 糊帧判据,只作诊断不进选帧,理由见 loop docstring)

postprocess/  帧 → 交付级序列帧
  pixelate  母版是像素画时吸附母版网格 + 锁母版色板,否则通用量化
  pack      脚线对齐 / sprite sheet / GIF
  rootmotion 逐帧时长(关键帧加长定格,等时长会让动作发飘)

prompt/ + master_prep.py  按动作类型选提示词、按动作预处理母版

两处实测挣得的修复一并带上:

1) 画布横向裁切(postprocess/pack.py)
   align_bottom_center 的三条缩放分支只按高度定标,是"主体是纵向长条"的人形先验。
   横向长条主体按同一系数缩放后宽度超出 cell,被 alpha_composite 以负 dest 静默丢像素,
   PIL 不报错。裁切悬崖 w/h ≈ 1.61;实测狐狸母版 w/h=1.78 丢 27px(鼻尖+尾尖),
   w/h=2.0 只剩 79.9% 内容。加宽度兜底 fill_w=0.96;人形 w/h 0.3–1.1 时该约束恒不生效,
   产物逐像素不变。

2) 步态周期误检(slicing/loop.py)三个坑,四段真 i2v 视频实测
   a. 角色整体平移让 d(p) 单调上升,argmin 滑到搜索窗边界交出假周期。
      实测骷髅走路:不消平移时曲线 40/56 段在上升,只剩 22/42/52 三个浅坑,argmin=22;
      加 _deskew 消平移后整条曲线只剩一个局部极小,正是真周期 56(凹陷深度 2.68)。
   b. 搜索窗上界 n//2 把真周期挡在窗外(待机真周期 62 > pmax 60)。改为 total*0.6。
   c. 谐波:22 接近真周期的一半,半周期闭环 = 末帧接回首帧时左右腿瞬间互换。
      改为在基周期整数倍里按归一化接缝复选,优先最小倍数。
   测不到可信凹陷(prominence < 0.25)时判"无周期",退化成全片均匀取、不硬闭环——
   实测骑士待机只有 31 帧,旧算法曲线单调、argmin 落在搜索窗下界 6 交出边界假值。

实测对照(n=16,接缝 = 末→首差 ÷ 组内相邻差均值,越接近 1 越闭合)
  骷髅走路 3.07→1.96 | 骑士走路 1.29→0.87 | 骑士待机 9.31→1.39 | 骑士奔跑 1.77→0.81
  待机那条最直观:旧算法写出的 GIF 只有 6 帧——16 帧里 10 帧逐像素重复,被 PIL 自动去重。

消融:改善全部来自 _deskew + 谐波复选。追加的"死帧避让 + 冻结裁剪"两个样本无变化、
两个变差(奔跑接缝 0.81→2.00),已回退,quality 只留作诊断。

* chore(tests): 删掉 rebase 带回来的 FAL 测试文件

FAL 队列面已随 1024XEngineer#179 移除,这个测试文件也一并删了。rebase 到新 main 时它被重放回来,
而它引用的 8 个 FAL 符号已不存在 —— 收集期直接 ImportError。

* feat(framework): 补 provider 抽象接口与抠图/视频实现

providers/ 此前只有三个 create_*_client 工厂,没有可供上层依赖的抽象类型,
ai_engine 无法在不 import 具体实现的前提下声明它需要什么能力。

- interfaces.py:ImageProvider / VideoProvider / MatteProvider 三个 Protocol,
  零依赖,供上层按能力而非按厂商声明依赖。
- matte.py:OnnxU2NetMatteProvider,onnxruntime 直跑 u2netp。不用 rembg:其底层
  同样依赖 onnxruntime,且 numba 老链在 3.12 无轮子。onnxruntime 导入失败时降级
  到 Pillow 兜底而非崩溃。
- sufy.py:SufyImageProvider / SufyVideoProvider。视频成品下载加三次退避重试与
  长度校验 —— 该步发生在提交任务、轮询、等待全部成功之后,此时费用已产生、视频
  已生成好,只差取回数据,连接断一次整单作废。实测同一角色连续两单死在这里各烧
  一次费用。test_sufy_video_download 的四条断言拿修复前的旧实现做过对照,确认其中
  三条在修复前会失败。

依赖声明:
- qiniu>=7.14 —— 此前未声明,镜像能起、/docs 也 200,只有第一次 POST /media/upload
  才 ModuleNotFoundError。
- onnxruntime>=1.17,<1.24 —— 1.24 起不再发布 macOS Intel(x86_64) wheel,Intel Mac
  装不上。1.23.x 仍覆盖 Intel/arm64/Linux + py3.12,API 一致,抠图代码零改动。

本 PR 不依赖其他未合分支:providers 不 import windup_common.models。

* refactor(ai_engine): 收拢 PNG bytes ↔ PIL 的转换,并重新 stack 到三个前置分支

管线内部按 PIL.Image 处理,跨模块边界(strategy → generator → ports 出参)按 PNG bytes
传递。这对转换此前在 strategy/concrete.py 与 impl/character_generator.py 各写了一份完整
拷贝,收成 _imgio.py 唯一定义(to_png / from_png)。编码参数一旦分叉,会在"某些帧丢了
alpha"这类只在画面上体现、不报错的地方出问题。

同步 stack:本分支重新对齐到 feat/character-domain-models、feat/provider-interfaces-and-matte、
feat/ai-engine-frame-toolkit 的当前终态,同名文件与三者逐字节一致。

* feat(ai_engine): 母版入口预检 + 出参成色信号 + 抠图跳过编码器边缘伪影

两头各加一道闸,方向相反:进门那道在**花钱之前**挡住不可能生成好的输入;
出门那道在钱已花完之后,让上层看得出"这次生成得怎么样"。

此前 ports 与 impl 里所有 raise 都在输出侧,对 master 不做任何前置判定。
2026-08-07 实测:喂一张"人物在画板前作画"的图请求 walk,全程无一处报错,
16 帧构图完整的错角色出完、钱花完。

check_master 判三类**本地零成本可判**的形态问题,不通过抛 MasterRejected:
- UNDECODABLE 不是图 / 截断
- NO_SUBJECT 全透明或全同色,没有可动的东西
- SUBJECT_TOO_SMALL 包围盒最短边 < 8px(放大 20 倍是色块不是角色),
  或主体占比 < 0.1%(对角散落两粒噪点会把包围盒撑到整幅,边长检查全过)
- ASPECT_TOO_WIDE 主体 w/h 超阈值,方形画布只能把角色硬缩成一条

REJECT_ASPECT 由交付画布几何推出(2*FILL_W/FILL_H)而非拍脑袋,并有测试锁住
这个推导关系——改了 pack.py 的填充比而这里不动,预检会放行一批下游装不下的母版。

MasterRejected 带机器可读的 code:server 据此选文案、判 4xx-不重试,与
NotImplementedError / 其他 ValueError(引擎侧问题,5xx,要人介入)分工明确。
判不了的(画的是不是角色、朝向对不对)不在此列,模块 docstring 写清"本层不判什么"。

GeneratedAction 此前只能表达"生成完了",不能表达"生成得怎么样":一段每帧都一样的
walk 与一段步态干净的 walk,帧数 / 时长 / fps 完全相同,调用方分辨不出。

三个字段各自不可由其他两个推导:
- motion_scale 相邻帧差的**绝对**尺度。必须单独给:dead_frame_mask 两条判据都是
  相对的,整段冻结时 d 全为 0、两条不等式变成 0<0,一帧死帧都报不出(实测 12 帧
  全同报 0 死帧)——相对判据天生看不见"整体没动"。
- dead_frames 死帧下标(不是 numpy 掩码:跨出 ai_engine 的契约要"哪几帧")
- loop_seam 末帧接回首帧的跳幅 ÷ 相邻帧平均步长。在**对齐之后**量,量的是用户真正
  看到的那组帧;分母为 0 返回 None 而不是 0.0——0.0 会被读成"完美闭环"。
  一次性动作(jump/attack)不给:首尾姿态本就不同,给个必然难看的数会诱导错误决定。

刻意没有糊帧率:2026-08-05 实测 6 段真 i2v 没有一帧糊帧,加进来是恒等于 1 的常数。

引擎只如实报数、不代替上层判决:交付 / 重试 / 换母版是产品决策,阈值该由 server 按
场景定;且到这一步钱已花完,引擎单方面丢弃产物只是把损失变成两份。

底色采样此前贴边取。视频帧最外一两行/列常是**编码器边缘伪影**而非底色:实测 9 段真
i2v × 16 帧 = 144 帧,贴边采样时 26 帧(18%)被判"底不均匀"而跳过清理——底色清理在
真实路径上等于从不生效。逐一查证全部由最外圈造成(某视频最右一列整列纯黑 std 50.4,
待机视频最顶一行 std 8.4 恰好压线越过 8)。往里让 2px 后 144 帧零误跳,三张静态母版的
取样中位色一个字节未变。

17 条新用例。变异测试 6/6 全部被捕获:阈值改成硬编码、去掉占比检查、去掉最短边检查、
motion_scale 恒返回 1、loop_seam 分母为 0 时返回 0.0、贴边采样。

其中"去掉最短边检查"最初**没被杀**——样本用的小方块占比也不达标,占比那条接住了它。
换成细长条(占比 1.3% 远超下限,只有边长这条能拦)后才真正独立。写完就绿的测试等于没写。

* fix(ai_engine): 出参只留一个播放时序真相源,并同步上游两处修复

机器审在本 PR 报的三条 P2,两条同源:契约里存在"能填/能读、但与另一处矛盾或不生效"的
字段。

一、GeneratedAction.fps 删除。它抄自入参,而 durations 按动作查表得来,两者描述同一段
   素材的不同播放速度:fps=20 宣称 50ms/帧,walk 实际给 125ms/帧,取哪个看消费方心情。
   逐帧 ms 严格更能表达(关键帧定格),所以保 durations、删 fps;真要单一帧率由消费方算。
   连带删除 ActionSpec.fps(在 feat/character-domain-models 里,本分支同步)。

二、删掉一条为缺陷背书的测试。此处曾有 test_loop_mode_currently_changes_nothing,把
   "传 pingpong / none 不改变任何一帧"钉成可执行事实,理由是"将来真接线时它会变红提醒
   删注释"。那是把缺陷固化:调用方能为一段往返动画付费、拿到一段线性循环,而测试为这个
   行为背书。现改为断言字段确实不存在——ActionSpec.loop 与 LoopMode 都已移除。
   同理,test_generate_walk_is_wired_end_to_end 里的 `assert out.fps == action.fps`
   换成断言时长确实来自动作查表(walk = 125ms/帧)。

三、抽帧改流式(改动本体在 feat/ai-engine-frame-toolkit,本分支同步)。121 帧 720p 真实
   视频抽 16 帧,进程 RSS 峰值 488 → 126 MiB。

变异测试:把 GeneratedAction.fps 加回去 1 条红;把 durations 改成固定 50ms 不查表 1 条红。
CI:ruff / import-linter 2 contracts / pytest 276 passed。

* fix(ai_engine): 母版预检的比例上限跟着交付画布走,非方画布不再判宽了

上一个提交让交付画布可以非方,这条紧接着补上被它架空的东西:master_check 的
REJECT_ASPECT 推导默认画布是方形 —— FILL_W 与 FILL_H 是**同一条边长**的两个比例。
画布能非方之后前提不成立了,同一条推导做下来是

    R = 2 * (cw/ch) * FILL_W / FILL_H = REJECT_ASPECT * (cw/ch)

不跟着收的后果正是这条阈值最怕的那件事:**预检按方形判、出帧按非方出**。
2026-08-11 实测,一个刚好过检(w/h=3.0968)的主体在各档画布上的交付占高:

    256×256   0.3086      512×512  0.3105      1024×1024  0.3105
    384×512   0.2324  ← 阈值本意保证的下限是 FILL_H/2 = 0.31,被架空

新增 reject_aspect_for(canvas) 算实际上限,check_master 收可选 canvas,
CharacterGenerator 把**出帧用的同一个 canvas** 传给预检。
canvas=None 或方形画布时与本提交之前完全一致(用例钉死 128/256/512/1024 四档
以及 None 都等于原 REJECT_ASPECT)。

修好之后的不变式实测(源画幅放大到 3000×600 杜绝主体被源边界裁掉;处在各自比例
上限的主体,交付占高应恒等于 FILL_H/2 = 0.31):

    256×256 上限 3.0968 → 0.3086      512×512 上限 3.0968 → 0.3105
    1024×1024 上限 3.0968 → 0.3105    384×512 上限 2.3226 → 0.3105
    512×384 上限 4.1290 → 0.3099      128×192 上限 2.0645 → 0.3125
    640×480 上限 4.1290 → 0.3104      2048×2048 上限 3.0968 → 0.3101
    与 FILL_H/2 的最大偏差 0.0025(取整噪声量级)

即窄高画布收紧、宽扁画布放宽,两侧都回到同一条几何。

变异测试(5 个变异逐个改坏 → 确认变红 → 还原,全部被杀):
  M1 非方画布不收紧阈值            → narrow_canvas_tightens 等 3 条红
  M2 宽高比取倒数(方向反了)      → narrow_canvas_tightens 等 3 条红
  M3 方形画布也被改动              → square_canvas_is_unchanged 红
  M4 判定仍用写死的 REJECT_ASPECT  → check_master_uses_the_canvas 红
  M5 预检不吃 canvas               → precheck_and_output_share_geometry 红

M5 一开始杀不掉(没有任何用例覆盖"预检与出帧用了不同 canvas"),补
test_precheck_and_output_share_the_same_canvas_geometry 之后才杀掉 —— 取一个夹在
方形阈值与 384×512 阈值之间的母版,方形放行、窄高必拒。

**依赖上游分支**:同 013520f,需要 feat/ai-engine-frame-toolkit 的 5da358e。
在工作区打上该提交的 pack.py 后跑,CI 全绿:ruff / lint-imports(2 contracts kept)
/ pytest 288 passed。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: rebase 到新 main 后取回被基座版覆盖的 ai_engine 实现

1024XEngineer#179 合入 main 后重排本分支,解冲突时对 7 个文件取了基座版,把本分支自己的实现覆盖了:
character_generator 丢了母版预检与成色量化(退回 123 行前的旧版)、prompt 三个模板丢了
装备参数化、concrete 丢了 canvas 传递、impl/__init__ 整个丢失。

同时把 framework/pyproject.toml 与 uv.lock 取回 main 版再重锁:本分支的旧 lock 少 254 行、
且 pyproject 删掉了 1024XEngineer#179 已入库的 passlib/redis/resend 三条声明,导致 bcrypt 找不到。

现在 lock 相对 main 是纯新增 43 行(imageio + av)。302 passed。

* fix(ai_engine): 进度上报统一到一个刻度,不再倒退

评审实跑逮到的:generator 按 i/4 报,中间夹着的 strategy.derive 按 i/3 报到**同一个**
ProgressPort 上。消费方按 i/total 画条会看到倒退两次 —— route 25.0% → derive 0.0%、
derive 66.7% → lastmile 50.0%,totals 同时出现 3 和 4。一个量两个真相源,取哪个看
消费方心情,与本分片删掉 fps / loop / palette 是同一条理由。

改法取评审给的第一条(generator 传偏移):generator 独占全局刻度 total=10,strategy 的
子进度由 _BandProgress 线性映射进 derive 区间 [2,7]。

- strategy 侧零改动。适配器只读它每次调用时自报的 total,不要求它声明自己有几步——
  声明值与实际值又是一对可以对不上的真相源。也不让它知道外层有几步:它是可插拔件,
  各路线步数本就不同。
- 刻度取 10 不取 5,是为了给 derive 段留出中间刻度;否则子进度全落同一格,虽不倒退
  但最慢的那段整段不动。
- 修后序列:0 → 10 → 20 → 30 → 50 → 80 → 90%,单一 total,零倒退。

测试 +3:同一次生成只允许一个 total、进度非递减、子进度必须落在 derive 区间内且
区间内确实动过(只断言"不倒退"的话,把适配器换成"永远报区间起点"也能过)。这三条
打在修复前的代码上全部 FAIL,报的正是评审给的那两处倒退。

一处如实说明:ProgressPort 的 docstring 写的是"server 转 SSE / 轮询状态",但 1024XEngineer#182 目前
唯一的实现是 executor.py:137 的 logger.info,SSE payload 里没有进度字段。所以今天这个
倒退只落在日志里,还没被用户看到 —— 也正因为没有活消费方依赖 total==4,才能直接改刻度
而不必兼容旧值。

Refs 1024XEngineer#171 1024XEngineer#53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: johnnyzhang-eng <johnnyzhang-eng@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(quota): 定义积分模块 ORM 模型、枚举与服务接口

- CreditAccount: 积分账户(balance/frozen/total_earned/total_spent)
- CreditTransaction: 积分流水(不可变账本,UNIQUE(ref_id, reason) 幂等)
- InviteCode / InviteRecord: 邀请码与邀请记录
- TokenUsage: Agent token 用量记录
- CreditReason / BillingMode 枚举 + 定价常量
- QuotaService 抽象接口(预付费冻结/扣减/解冻、后付费原子扣减、邀请码、入账)

* refactor(quota): 定价常量改为配置化

- 新增 framework/config/quota.py: QuotaSettings (pydantic-settings)
- 环境变量前缀 QUOTA_,支持 .env 注入
- enums/quota.py 只保留枚举,不再硬编码定价
- .env.example 新增积分定价配置项

* refactor(quota): 定价配置化 + 暂不实现后付费/邀请码

- 新增 framework/config/quota.py: QuotaSettings (pydantic-settings, QUOTA_ 前缀)
- enums/quota.py 只保留枚举,定价从环境变量读取
- 注释掉 InviteCode/InviteRecord/TokenUsage ORM(枚举已预留)
- 注释掉邀请码端点和后付费 deduct_postpaid
- .env.example 新增积分定价配置项

* fix(quota): 移除未使用的导入 (ruff F401)

* test(quota): 补充测试用例,覆盖率达 100%

- Service 层:余额查询、冻结/扣减/解冻、入账、流水查询、边界异常
- API 层:余额端点、流水端点、分页参数、无账户 404、未登录 401
- 完整预付费流程:冻结→扣减、冻结→解冻、多任务并发

* chore(quota): 移除 token_rates_json 配置项(后付费暂不实现)
* feat(workspace): add pixel entrance artwork

The workspace needs a distinct visual cue for each creation path.

Add the four restrained pixel-art assets as one reviewed set.

Workspace entrances can now reveal their individual theme colors.

* feat(workspace): add creation workspace

Creators need one fixed entry point for their existing production context.

Add the four-path workspace with real project, workflow, character, and outfit data.

Users can resume work without mock content or a scrolling dashboard.

* refactor(playtest): share outfit playback state

The workspace and Playtest entry need the same definition of a playable outfit.

Extract frame counting into the character entity and reuse it from Playtest.

Both entry points now derive playback availability from real frames.

* test(workspace): cover workspace interactions

The new workspace coordinates several real navigation and context paths.

Cover the fixed layout, entrance artwork, data states, retries, and selections.

Regressions in the workspace routing surface are now caught locally.

* test(playtest): cover outfit playback state

Playback availability must depend on actual frames rather than action metadata.

Cover outfits with and without generated animation frames.

The shared entity rule is protected independently of either page.

* refactor(home): remove obsolete home interface

The old placeholder home interface is no longer part of the product route.

Remove its page, decorative bird, and choice card implementation.

The upcoming landing page can own the root route without dead UI code.

* test(home): remove obsolete home coverage

The deleted placeholder home interface no longer has behavior to preserve.

Remove its page and choice-card test files with the retired surface.

The test suite now reflects the active landing and workspace boundaries.

* feat(workspace): register protected workspace route

The landing page must retain the root route while creators enter a separate workspace.

Register WorkspacePage at the protected /workspace path.

Authenticated navigation now has a stable workspace destination.

* test(workspace): cover dedicated workspace route

The workspace route must remain separate from the public landing page.

Cover authenticated access to /workspace and retain the auth boundary assertions.

Future route changes cannot silently put the workspace back at the root.

* style(workspace): format workspace page

Frontend CI requires the workspace page to match the repository formatter.

Apply the formatter to the two multiline conditional class expressions.

The page now passes the targeted formatting check without behavior changes.

* fix(workspace): restore workflow entrance choices

The workflow card lost its separate create and resume paths during the workspace redesign.

Split the existing card copy into two accessible actions while preserving the artwork and restrained motion.

Users can create a project through the original route or resume a real workflow from the workspace context.

* test(workspace): cover workflow entrance choices

The workspace coverage still targeted the removed single workflow selector.

Assert the project creation route and drive resume scenarios through the renamed action.

The tests now protect both entrance paths and the existing real-run selection behavior.

* fix(workspace): preserve entrances on narrow screens

The desktop grid compressed the primary workspace entrances below usable widths on phones.

Collapse the layout to one column and hide the contextual sidebar below the medium breakpoint.

The fixed workspace canvas now keeps all four entrances available on narrow screens.

* test(workspace): cover narrow-screen entrances

The workspace suite did not protect the responsive boundary for its primary navigation.

Assert the single-column layout and hidden contextual sidebar used below the medium breakpoint.

Future layout changes now preserve usable narrow-screen entrances.

* fix(workspace): remove unsupported project sorting claim

The workspace described projects as update-sorted while the API currently returns identifier order.

Replace the unsupported ordering claim with a neutral project-list label.

The interface now matches the data contract without expanding backend scope.

* test(workspace): cover neutral project list label

The workspace suite allowed copy to promise an ordering that the project API does not provide.

Assert that the update-sorting claim is absent and the neutral project-list label is rendered.

Future copy changes now remain aligned with the current API contract.
* feat(framework): 补 provider 抽象接口与抠图/视频实现

providers/ 此前只有三个 create_*_client 工厂,没有可供上层依赖的抽象类型,
ai_engine 无法在不 import 具体实现的前提下声明它需要什么能力。

- interfaces.py:ImageProvider / VideoProvider / MatteProvider 三个 Protocol,
  零依赖,供上层按能力而非按厂商声明依赖。
- matte.py:OnnxU2NetMatteProvider,onnxruntime 直跑 u2netp。不用 rembg:其底层
  同样依赖 onnxruntime,且 numba 老链在 3.12 无轮子。onnxruntime 导入失败时降级
  到 Pillow 兜底而非崩溃。
- sufy.py:SufyImageProvider / SufyVideoProvider。视频成品下载加三次退避重试与
  长度校验 —— 该步发生在提交任务、轮询、等待全部成功之后,此时费用已产生、视频
  已生成好,只差取回数据,连接断一次整单作废。实测同一角色连续两单死在这里各烧
  一次费用。test_sufy_video_download 的四条断言拿修复前的旧实现做过对照,确认其中
  三条在修复前会失败。

依赖声明:
- qiniu>=7.14 —— 此前未声明,镜像能起、/docs 也 200,只有第一次 POST /media/upload
  才 ModuleNotFoundError。
- onnxruntime>=1.17,<1.24 —— 1.24 起不再发布 macOS Intel(x86_64) wheel,Intel Mac
  装不上。1.23.x 仍覆盖 Intel/arm64/Linux + py3.12,API 一致,抠图代码零改动。

本 PR 不依赖其他未合分支:providers 不 import windup_common.models。

* feat(framework): 补 provider 抽象接口与抠图/视频实现

providers/ 此前只有三个 create_*_client 工厂,没有可供上层依赖的抽象类型,
ai_engine 无法在不 import 具体实现的前提下声明它需要什么能力。

- interfaces.py:ImageProvider / VideoProvider / MatteProvider 三个 Protocol,
  零依赖,供上层按能力而非按厂商声明依赖。
- matte.py:OnnxU2NetMatteProvider,onnxruntime 直跑 u2netp。不用 rembg:其底层
  同样依赖 onnxruntime,且 numba 老链在 3.12 无轮子。onnxruntime 导入失败时降级
  到 Pillow 兜底而非崩溃。
- sufy.py:SufyImageProvider / SufyVideoProvider。视频成品下载加三次退避重试与
  长度校验 —— 该步发生在提交任务、轮询、等待全部成功之后,此时费用已产生、视频
  已生成好,只差取回数据,连接断一次整单作废。实测同一角色连续两单死在这里各烧
  一次费用。test_sufy_video_download 的四条断言拿修复前的旧实现做过对照,确认其中
  三条在修复前会失败。

依赖声明:
- qiniu>=7.14 —— 此前未声明,镜像能起、/docs 也 200,只有第一次 POST /media/upload
  才 ModuleNotFoundError。
- onnxruntime>=1.17,<1.24 —— 1.24 起不再发布 macOS Intel(x86_64) wheel,Intel Mac
  装不上。1.23.x 仍覆盖 Intel/arm64/Linux + py3.12,API 一致,抠图代码零改动。

本 PR 不依赖其他未合分支:providers 不 import windup_common.models。

* feat(ai_engine): 对外契约 ports + 动作分流 strategy + 串联 CharacterGenerator

server 与生成引擎之间的唯一边界,以及"哪个动作走哪条生成路线"这个架构决策。

ports/  server 只 import 这里,由 CI 的 import-linter 分层门禁强制。
  CharacterGeneratorPort.generate(card, action, master, progress) -> GeneratedAction
  边界:ai_engine 只产出帧 bytes + 逐帧时长,不碰存储 / 数据库 / 任务状态。母版由
  server 从 Character.reference_image_url 取好以 bytes 传入;产出的帧由 server 上传
  对象存储、写 character_data。依据是"谁掌握租户与配额上下文"——bucket、路径规则、
  归属项目、配额全在 server;ai_engine 自持存储等于把租户概念下沉到一个只做图像计算
  的层。代价是帧 bytes 在内存过一次(16 帧 512×512 RGBA ≈ 16MB,可接受)。

strategy/  ROUTE_MATRIX 是实测挣得的架构契约,改它 = 改产线。
  walk / run / jump / attack / idle -> VIDEO_I2V;hit -> PER_FRAME
  依据:逐帧独立生成锁不住"哪条腿在前"(踢踏舞),视频天生连贯、腿自然交替;
  hit 这类离散姿势单帧可编辑价值高、无连续步态。Refs 1024XEngineer#35 1024XEngineer#53。

impl/CharacterGenerator  选路线 -> strategy.derive 出帧 -> 脚线对齐 -> GeneratedAction。

与 1024XEngineer#53 原设计的两处差异:

1) idle 从 PROC_IDLE 改走 VIDEO_I2V,GenRoute.PROC_IDLE 与 ProcIdleStrategy 一并移除。
   1024XEngineer#53 原设计 idle 走 ¥0 的程序化局部呼吸(Idle-B),实测做不出可用效果,放弃,认这份
   i2v 的钱。不留没有实现的枚举值。

2) 未实现的路线抛错,不返回空帧。
   旧桩实现 return [b""] * n_frames,调用方拿到的 GeneratedAction 帧数对、时长对、
   无异常——完全像一次成功的生成。server 会把 N 个 0 字节文件传上对象存储、写进
   character_data,用户看到 N 张裂图,排查时不会想到是路线没实现。
   现在 PerFrameStrategy 调用即抛 NotImplementedError;装配表缺该路线时抛错并报出
   已装配了哪些;strategy 吐出空帧时抛 ValueError。四条回归测试拿旧实现对照过,
   确认在修复前全部失败。

另记录 ROUTE_MATRIX 形状的已知边界:它是「动作类型 → 路线」一对一映射,隐含前提是
"路线由动作的物理性质唯一决定"。该前提对逐帧 / 视频成立,但对渲染出帧路线不成立——
同一个 walk 走 i2v 还是走渲染,取决于该角色有没有 3D 模型,那是 server 才知道的事。
接入第三条路线前须先定「路线选择由谁决定」。

本分支 stack 在 feat/character-domain-models、feat/provider-interfaces-and-matte、
feat/ai-engine-frame-toolkit 之上,那三个合并后 rebase。

* fix(generation): 修机器审报的五处 P1

`_get_generator()` 里还留着 `GenRoute.PROC_IDLE: ProcIdleStrategy(...)`,而这两个都已
随「程序化待机放弃」删除。注入 generator 的测试走不到这条装配路径,所以测试全绿而真实
调用全崩。改为只装当前 GenRoute 真有的路线,并加一条漏装断言——将来新增枚举成员时会
在装配处立刻暴露,而不是等某个动作第一次被请求。

`_download_master` / `_download` 直接 `httpx.get(input.reference_image_urls[0])`,
而那个 URL 来自已认证请求的请求体。等于把服务器当跳板:打 loopback 绕过鉴权中间件、
读云实例元数据服务的临时凭证、探测私网拓扑;重定向还能把合法域名换成上述任意一种。

新增 `_fetch.fetch_own_media`:白名单(必须是 `storage_settings.download_base` 前缀)
+ 禁跟随重定向 + 响应体上限 16 MiB(边读边计数,不信 Content-Length)。

取白名单而非黑名单:黑名单要穷举 127/8、10/8、172.16/12、192.168/16、169.254/16、
::1、fc00::/7 以及各种十进制/八进制/IPv6-mapped 写法,漏一条等于没做。而本业务只需拉
自家 bucket 的图(母版与参考图都先经 /media/upload 传上去)。代价是不能再传外部图床
链接——真要支持该走一个显式的「导入外部素材」入口,在那里做完整校验与配额。

`task_repo` 一律发 `task_update`,而 stream 的 `_TERMINAL_EVENTS = {"completed","failed"}`
永不匹配,于是终态 break 走不到。端点带 `retry: 3000`,浏览器原生 EventSource 每 3 秒
重连、每次重收同一条 completed。按状态映射事件名。

`publish` 被后台 daemon thread 调用(executor → task_repo),而队列属于处理 SSE 请求
的那个 loop。`asyncio.Queue` 不是线程安全的。订阅时记下所属 loop,发布时经
`call_soon_threadsafe` 回到那个 loop 再入队;loop 已关闭时静默丢弃(任务状态已落库,
重连后靠 GET /tasks/{id} 取,抛异常会把后台任务整个带崩)。

`num_images` 直通 provider 调用循环,请求模型不设上限——一个已认证请求填个大数就能绕过
按请求计的限流、把成本拉到无上限。加 `ge=1, le=4`;`num_frames` 加 `le=64`;宽高加
`64..2048`。

顺带删掉请求体里的 `user_id: int = Field(gt=0)`:端点已改从 `request.state.current_user`
取归属,这个字段既不被读、又让调用方以为能指定归属者——填别人的 id 不报错也不生效。

20 条回归用例。变异验证中**逮到自己两条摆设测试**并已重写:
- 终态事件名那条原先直接读 `_STATUS_EVENT` 字典,而变异改的是 `_publish_task_update`
  里的用法 → 改为注入假 bus、走真实调用路径断言发出的事件名;
- 跨线程那条证不出 `call_soon_threadsafe` 的必要性(实测裸 `put_nowait` 在单队列场景
  也能被 `get()` 取到,CPython 有元素时走快路径)→ 如实在 docstring 写明本用例强度,
  另补一条「订阅必须记下所属 loop」的结构断言,那条能杀死变异。

漏装断言那条变异存活是**预期**:当前两个路线都装满,`missing` 恒为空集,它是防未来
回归的守卫而非当前行为,测试锁的是「装满」这个事实。

* fix(generation): 请求里的图片尺寸真的生效,并同步上游 provider 两处修复

对抗复查发现:width / height 从请求进到 CharacterImageInput、被 _validate_project_size
校验过,然后被丢掉 —— ImageProvider.gen_image 没有尺寸参数,模型出多大就返多大。调用方
要 512×512、拿到 1024×1024,而请求被接受了。性质与本轮删掉的 ActionSpec.fps / loop 完全
相同,只是这次字段在入口侧。今天还给这两个字段加了 le=2048 上界,等于替它们背书。

模型本身不吃宽高,所以在编排层落实:复用已有的 _fit_to。给它加 smooth 参数——序列帧是
像素画必须 NEAREST(插值会把硬边糊成灰边并引入调色板外的颜色),全彩角色母版反过来,
NEAREST 缩图明显锯齿,用 LANCZOS。

同步上游(改动本体在 feat/provider-interfaces-and-matte):文生图路径改读配置里的
chat_completions_path;400/404 翻译成指向 GET {base}/models 的可操作错误。

测试 +4。变异测试:再把尺寸丢掉 2 条红,smooth 参数不接线 1 条红。其中重采样那条第一版
用纯色图作源是无效仪器(纯色下两种重采样结果完全相同),已换成棋盘格。

* fix(app): 交付尺寸传给引擎出帧,不再拿到帧之后再缩一次

编排层此前对引擎交付的每一帧再做一次 _fit_to(png, sprite_w, sprite_h)。引擎恒出
256,项目要 512 时这就是二次重采样 —— 而实际后果比"糊一次"严重得多:_fit_to 用
Image.thumbnail,**thumbnail 只缩不放**。2026-08-11 复刻这段逻辑实测(喂主体高
157px、脚线 0.92 的 256 交付帧):

    目标 512×512 → 主体仍 157px(根本没放大),脚线 0.92 → 0.709
    目标 384×384 → 主体仍 157px,              脚线 0.92 → 0.779
    目标 128×128 → 主体 78px,                脚线 0.914(缩小方向正常)

即放大方向上主体一点没变大("成品放大看很糊"的直接来源),并且引擎刚用
align_bottom_center 对齐好的脚线被整体挪高,角色不站在地上,ref_height 那套跨动作
本体尺寸一致也一并失效。

改法:把项目 sprite 尺寸作为 canvas 传给 generate,引擎一次出到位,那一步不存在了。

用归档角色「林间斥候」的真实抠图帧(1280×720,主体高中位 619px)端到端实测:

    canvas 不传        交付 256×256   主体高中位 159px
    canvas (256,256)   交付 256×256   主体高中位 159px(与不传**逐字节相同**)
    canvas (512,512)   交付 512×512   主体高中位 318px —— 倍数 2.000
    canvas (384,512)   交付 384×512   主体高与 (512,512) 完全相同(高度几何只看高)
    canvas (1024,1024) 交付 1024×1024 主体高中位 635px

顺带一条选型信息:源帧主体 619px,512 档交付 318px 仍是**下采样**(不引入插值糊),
1024 档 635px 已经越过源分辨率、是上采样,收益递减。

_fit_to 换成 _require_size:只核对、不补救。尺寸对不上说明生成侧没按 canvas 出帧,
该报错让人看见,而不是缩放补边把问题抹平、交付一批脚线错位的帧。_fit_to 本身保留,
角色母版那条路径(smooth=True)仍在用。

**接口影响**:CharacterGeneratorPort.generate 多了 canvas 入参,实现该 Protocol 的
测试替身必须跟着接。修 _SpyGenerator 时顺带发现一个假绿用例:该 spy 一直在传
GeneratedAction(fps=...),而该字段早已删除,构造直接 TypeError、任务其实被判 FAILED;
当时的用例只断言 seen_facing(在构造之前就赋了值)所以一直绿着。已一并修好,
test_project_perspective_constrains_facing 现在跑的是一次真正成功的任务。

变异测试(4 个变异逐个改坏 → 确认变红 → 还原,全部被杀):
  E1 不把项目尺寸传给引擎     → project_sprite_size_is_passed 等 2 条红
  E2 canvas 宽高接反          → non_square_sprite_size_passed 红
  E3 只传宽当成方形           → non_square_sprite_size_passed 红
  E4 尺寸不符时静默放行       → wrong_size_fails_instead_of_rescaled 红

**依赖上游分支**:canvas 入参由 feat/ai-engine-ports-and-strategy 的 013520f 提供,
后者又依赖 feat/ai-engine-frame-toolkit 的 5da358e。本分支尚未同步这两个提交,故
单独跑 pytest 会有 1 条 test_action_task_runs_end_to_end 失败(真实
CharacterGenerator 还不认 canvas)。在工作区先打上那两个提交再跑,CI 全绿:
ruff / lint-imports(2 contracts kept) / pytest 309 passed。同步后即恢复。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(generation): SSE 订阅补归属校验与终态预检,去掉无消费方的 project_id

人工评审报的三条,逐条修:

一、越权订阅。stream 端点只按 task_id 订阅、不看是谁在订阅,任意已认证用户猜到 id
   就能拿到别人任务的实时进度与最终产物 URL(事件体带 result,即帧的对象存储地址)。
   同文件的 GET /tasks/{task_id} 有校验,stream 漏了,而两者从没被放在一起测过。
   校验放在 subscribe **之前**:放之后的话越权请求仍会在 EventBus 上挂一个订阅者,
   照样收事件、只是响应体被丢弃,订阅表还会因为没人 unsubscribe 而增长。

二、终态预检。原先是一行 TODO,而上一行的 docstring 已经承诺了该行为 —— 读文档的人
   不会发现,实际表现是客户端要先挂满 30 秒心跳超时才拿到终态。

三、project_id 声明为必填但从未被使用,归属判定的依据是任务自己的 user_id,不是调用方
   声称的项目。删掉后 fastapi.Query 也变成未使用 import,反过来印证它确实没有消费方。

顺带:事件 payload 从 task_repo 抽成公开的 task_event_payload()。终态预检是第二个发送
点,在 API 层再抄一份字段列表就是第二个真相源,加字段时漏一处会让客户端拿到两种形状的
同名事件。

测试 +9,5 条变异全部杀掉(去掉归属校验 / 去掉终态预检 / 校验挪到订阅之后 /
payload 少字段 / 终态映射把 failed 当成 completed)。

其中"payload 同形状"那条第一版是摆设:期望键集也用 task_event_payload 反算,两边同源,
删字段一起变、断言永远成立。改成把 SSE 事件体的键集写死成契约清单,并另加一条直接比
两条真实发送路径产出的用例。

过程中三处是测试自身写错、不是代码错,记下避免再犯:按 HTTP 状态码断言越权(本仓
BizException 统一以 200 + 业务码返回,把"校验生效"误判成"越权",且成功码是 200 不是 0);
create_task 签名靠猜;conftest 的建表清单里没有本 PR 新引入的 generation_task 表。

* fix(generation): SSE 归属补齐「任务属于项目」这一道,并落终态预检

**先更正我上一版的方向。** 上一版删掉了 `project_id`、改用任务自己的 `user_id` 做归属
校验,并把 EventBus 改成单键。那是错的:主线 1024XEngineer#110 里 `project_id` 正是归属校验的依据
(`_get_project_or_raise`),且 EventBus 按 `(project_id, task_id)` 双键隔离同一 task_id
在不同项目下的流。删掉它会退化主线已有的能力。本版改为**在主线骨架上补齐它的 TODO**。

一、归属补成两道。主线已校验「项目属于当前用户」,缺「任务属于那个项目」。缺这一道,
   任意已认证用户拿**自己的** project_id 配上别人的 task_id 就能订阅到别人的流,而事件
   体带 result,即最终帧的对象存储 URL。两道都在 `subscribe` 之前 —— 放之后的话越权请求
   仍会在 EventBus 上挂一个订阅者(照样收事件、只是响应体被丢弃),订阅表还会因为没人
   unsubscribe 而增长。

二、终态预检落地(原先是一行 TODO,而 docstring 已经承诺了该行为)。实际表现是客户端要
   先挂满一次心跳超时才拿到终态。

三、跨线程投递:`publish` 改成**同 loop 直接入队、跨 loop 才 call_soon_threadsafe**。
   一律走 marshal 是错的 —— 那是异步调度,要等 loop 下一次迭代才真入队,于是
   「publish 完立刻 get_nowait」会拿到空队列,主线 1024XEngineer#110 的项目隔离用例正是这么写的。
   跨 loop 分支保留是因为 executor 在 daemon thread 里跑,而 asyncio.Queue 不是线程安全的。

四、`task.project_id` 为空时记 warning 并早退,不再 publish 到一个没人听的键上。
   静默发出去的现象是「任务确实在跑、状态也在落库,但前端进度条一动不动」,日志里一行
   异常都没有。

顺带:事件 payload 抽成公开的 `task_event_payload()`。终态预检是第二个发送点,在 API 层
再抄一份字段列表就是第二个真相源,加字段时漏一处会让客户端拿到两种形状的同名事件。

测试 10 条,7 条变异全部杀掉(去掉「任务属于项目」/ 去掉终态预检 / 校验挪到订阅之后 /
payload 少字段 / 终态映射把 failed 当 completed / project_id 为空时静默 publish /
publish 一律走异步 marshal)。

其中「payload 同形状」那条第一版是摆设:期望键集也用 task_event_payload 反算,两边同源、
删字段一起变、断言永远成立。已改成把 SSE 事件体键集写死为契约清单。

* fix(generation): 三个端点接回服务层,并补上能发现"没接上"的测试

评审报的是实情:这三个端点在分支上还是 TODO 桩。根因是 8-11 那次 rebase 解冲突时对
generation.py 取了基座版,把本分支的实现换成了主线的桩,CI 全绿没拦住。

修复:
- POST /generation/image、POST /generation/action 接回 generation_service,落 PENDING
  记录后返回;后台线程仍走 _dispatch_after_commit(commit 后再起,否则后台 session
  读不到未提交的行、update 静默跳过、任务永远 PENDING)
- GET /generation/tasks/{id} 接回 task_repo,归属两道:项目属于当前用户 + 任务属于该
  项目。只查项目不够,任意已认证用户拿自己的 project_id 配别人的 task_id 就能读到别人
  的产物 URL。与 stream 端点同口径。

补 5 条测试,锁住"端点确实落库"“_task_to_out 确实被调用”“跨项目任务读不到”。
之前没有这类断言,桩返回 400、测试也断言 400,两边一致所以看不出来。
变异测试:端点改回桩 4 条红、去掉任务归属校验 1 条红、绕开 _task_to_out 4 条红。

顺带:conftest 的建表清单补上 generation_task(端点接上后才会真的用到这张表);
删掉 rebase 带回来的 test_fal_queue_video_provider.py(FAL 面已随 1024XEngineer#179 移除)。

* fix(deps): framework 依赖声明与 lock 取回 main 版

rebase 时删掉了 main 已入库的 passlib/redis/resend 三条声明,lock 也跟着丢了
bcrypt,CI 报 ModuleNotFoundError。本地 venv 恰好装着这三个包所以没暴露。

1024XEngineer#181 昨天修过同一处,当时没顺手检查 1024XEngineer#182。

* test(orchestrator): 补齐白名单取图的下载段 —— 三条防线此前一行没测

`codecov/patch` 在重排后掉到 83.91%(目标 84.10%,差 0.19pp)。1024XEngineer#181 合入 main 后
1024XEngineer#182 的 patch 只剩自己那 10 个提交,分母变了就压线掉下来。

去看未覆盖的行,最大缺口是 `_fetch.py` 的 46%(26 行里 14 行没跑),而那 14 行正是
**放行之后的三条防线**,一条都没测过:

1. **`follow_redirects=False`** —— 白名单最容易被绕开的方式:URL 本身完全合规,
   坏事发生在重定向之后。自家域名返回 302 指向 169.254.169.254,跟过去就等于白名单
   没写。新测试断言异常之外,还断言**元数据服务那个 URL 从未被请求过**。
2. **声明 Content-Length 超限** → 读 body 之前就拒。
3. **Content-Length 撒谎时边读边计数** —— 声明 1 字节实际吐 100MB,只信 header
   就能吃光 worker 内存。

四条新测试用 httpx.MockTransport,不联网。`_fetch.py` 46% → **100%**。

两处自己踩的坑,记下来:
- 补丁装在 `F.httpx.Client` 上,而工厂内部又调 `httpx.Client` → 无限递归。必须先把
  真的 Client 抓在局部变量里再打补丁。
- 重定向那条初版写的是 `pytest.raises(Exception)`,于是上面那个 RecursionError 也
  算"通过" —— 测试因为错误的原因变绿。已收紧成 `httpx.HTTPStatusError`。

变异测试验过这四条真的能咬:分别关掉重定向防护 / 废掉边读边计数 / 废掉声明超限检查,
三次都被逮到(脚本带 try/finally,结束校验 sha256 一致)。

Refs 1024XEngineer#171 · Refs 1024XEngineer#78

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: johnnyzhang-eng <johnnyzhang-eng@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(playtest): load the complete asset catalog

The Playtest entry previously treated the first API page as the complete project and character set.

Iterate every reported page before building the selectable outfit groups.

Playable outfits beyond the first hundred records now remain discoverable.

* fix(playtest): redraw the reduced-motion stage

Resizing a canvas clears its bitmap while reduced-motion mode intentionally stops animation scheduling.

Render a static frame from the resize path and avoid a duplicate initial draw.

The Playtest illustration now remains visible after layout changes.

* test(playtest): cover paginated asset discovery

The entry regression was not represented by the existing small account fixtures.

Exercise project and character datasets that cross the backend page-size boundary.

The test now protects complete asset discovery across later pages.

* test(playtest): cover reduced-motion resizing

The canvas test previously exercised only the continuously animated scheduling path.

Simulate reduced motion and invoke the resize observer after the initial static frame.

The regression test now detects a canvas that becomes blank after resizing.
* feat(character): 添加角色发布状态过滤

- 添加 CharacterStatus 枚举(DRAFT=0, PUBLISHED=1)
- 根据 character_data 自动推断发布状态(至少存在一条包含真实帧的动作即为已发布)
- 在 GET /characters 接口添加可选的 status 查询参数
- 创建和更新角色时自动计算 status
- 添加测试覆盖所有场景

Refs: 1024XEngineer#174

* fix(character): 限制 status 参数范围为 0 或 1

添加 ge=0, le=1 约束,防止传入无效值(如 status=2)返回空列表而非报错。

Refs: 1024XEngineer#174

* fix(character): 拒绝 character_data 为 null 的更新请求

当 PATCH 请求中 character_data 为 null 时返回 400 错误,
避免后续验证失败或数据不一致。

Refs: 1024XEngineer#174
* fix(workflow): open editor after project creation

Workflow project creation previously stopped at the asset workspace.

Create a real initial WorkflowRun and navigate with its returned identifier.

Keep ordinary project creation unchanged and make workflow retries project-safe.

* test(workflow): cover project-to-editor entry

The workflow entry regression needs protection across routing and API orchestration.

Cover source intent, initial nodes, real run navigation, and retry behavior.

Ensure retries never create a second project or depend on edited project fields.

* fix(workflow-editor): remove implementation badge

The project summary exposed an internal transport detail to creators.

Remove the WorkflowRun interface badge while retaining run and version context.

Keep the summary focused on project information users can act on.

* test(workflow-editor): cover concise project summary

The project summary should not expose internal interface wording.

Assert the summary remains available while the implementation badge stays absent.

Retain async error coverage without relying on removed copy.

* style(workflow): apply repository formatting

Frontend CI uses oxfmt rather than Prettier for formatting checks.

Apply the repository formatter to the workflow entry regression test.

Restore the branch formatting gate without changing test behavior.

* test(workflow-editor): stabilize prompt submission

The full coverage run could click before React committed the controlled prompt value.

Wrap the input event in an async act boundary before submitting the workflow command.

The persistence assertion now observes the intended prompt consistently.
* feat(header): unify the application header

The application chrome used split floating surfaces with inconsistent navigation and account interactions.

Replace it with a transparent blurred top bar, responsive navigation, text motion, and an animated account menu.

The shared header now stays consistent across product routes without hiding mobile navigation.

* test(header): cover unified header interactions

The shared header now includes replayable navigation motion and a stateful account menu.

Cover routing, motion replay, account menu entry and exit, responsive navigation, and the transparent top-bar surface.

The regression suite protects the accepted header behavior across future layout changes.

* test(account): align header nickname assertions

Account page coverage still queried the former direct account-center link for the synchronized nickname.

Assert the nickname on the new account-menu trigger, where the shared header now presents user identity.

The account integration tests now follow the unified header contract without changing production behavior.

* refactor(header): remove redundant wave cleanup

Text-wave replay already remounts glyphs through the play identifier.

Remove the animation-end state cleanup and its event handlers.

The visible motion remains unchanged while the interaction path is simpler.

* test(header): cover account menu edge cases

The unified header includes fallback identity and resilient logout paths.

Cover logout failure, email fallback, and animated account-link closure.

The focused suite now exercises the remaining header interaction branches.
模板里写死的装备名词是在断言该物件存在。母版没有斗篷时,模型会为了满足文字
凭空长一件,母版真有的特征(颈后软管)反被挤掉 —— 2026-08-11 拿一个完全无
布料的刚性角色实跑复现过。这与 ports.CharacterGeneratorPort 记的「身份由母版
承载,身份描述再写一遍反而会和母版打架」是同一条。

根因不是"漏传参数",是 garment/weapon/feet 三个参数**零写入方**:
strategy.concrete._build_prompt 只传 facing,于是每个角色都吃到那个持剑披风
原型的默认值("the cape and tabard" / "the sword")。walk 的 weapon 那句连
参数都没有,是写死的。

改法:
- 衣饰/手持物改成存在无关的保持句("whatever the character already wears
  or carries" / "anything held in the hands")。锁定效力不变 —— 有持物就锁住
  长度、握点、朝向;没有就不凭空造。attack 那四条已验证的锁定一条不少。
- boot → foot:同一类断言,会给赤足/高跟角色套上靴子。
- 删掉三个装备参数。留一个没人传的可选参数,只会让人以为该能力已经存在;要按
  角色定制装备文字,得先有地方存"这个角色穿什么拿什么",那是角色卡契约的事。
- master_prep.MASTER_POSES 同样处理:母版是整条 i2v 链的身份来源,污染会传到
  所有动作。

测试 +14:装备名词黑名单扫全部动作×朝向、扫 MASTER_POSES,签名断言只剩
facing,派生层不得再传装备。在修复前的代码上跑这 14 条,13 条 FAIL(唯一通过
的那条是 _build_prompt 的护栏 —— 那一层本来就是干净的,锅在默认值)。

Refs 1024XEngineer#195

Co-authored-by: johnnyzhang-eng <johnnyzhang-eng@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: minorcell <120795714+minorcell@users.noreply.github.com>
Co-authored-by: minorcell <120795714+minorcell@users.noreply.github.com>
fennoai Bot and others added 27 commits August 17, 2026 11:37
Co-authored-by: minorcell <120795714+minorcell@users.noreply.github.com>
* feat(projects): add visual project gallery

The project center needs a calmer asset-first browsing surface.

Render the create entrance, project previews, and empty pixel canvases while resolving real character preview images.

Existing project creation, deletion, pagination, and navigation behavior stays intact.

* test(projects): cover gallery preview behavior

The redesigned gallery must keep its real API and navigation boundaries explicit.

Cover character preview requests, empty canvases, creation links, deletion, and pagination.

The tests now assert user-visible behavior instead of styling internals.

* feat(workspace): add opening tagline

The workspace heading needs the same concise context used across the product surfaces.

Add the approved tagline directly beneath the existing page title.

No workspace navigation or data-loading behavior changes.

* test(workspace): cover opening tagline

The approved workspace context line should remain part of the rendered heading area.

Assert the tagline alongside the existing navigation behavior.

The focused coverage protects the copy without expanding workspace scope.

* test(projects): cover preview fallbacks

Codecov exposed untested project preview fallback branches.

Exercise reference images, first frames, and isolated request failures.

Keep gallery previews resilient without changing production behavior.

* fix(projects): bound preview fallback requests

---------

Co-authored-by: huyanxius <178478063+huyanxius@users.noreply.github.com>
* fix(auth): 内测期间关闭公开注册,仅保留已有账号登录

公开注册、注册发码和验证码自动建号都会在内测阶段误开账号。登录入口保留,测试账号由运维预置。

Fixes 1024XEngineer#326

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(auth): format closed-registration helper copy

oxfmt requires the login helper line to stay on one line. This unblocks Frontend CI format:check on 1024XEngineer#327.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auth): 撤回前端改动并补上验证码登录分支覆盖

公开注册只在后端关闭。前端注册入口保持原样,调用时由接口拒绝。补测封禁账号与未验证邮箱的验证码登录,补齐 codecov/patch 缺的两行。

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auth): 前端关闭注册入口并提示通过 Issues 申请

访客仍能看到注册按钮,但不可提交。登录面板引导通过 GitHub Issues 联系团队开通。

* fix(auth): 保留注册实现,只注解对外入口

内测仍关闭公开注册,但注册表单、切换逻辑和测试留在仓库里,方便以后重新开放。

* chore(openapi): 同步关闭公开注册后的接口描述

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(ui): extract shared kinetic copy

The account panel owned a reusable line transition inside the feature.

Move the existing renderer and styles into shared UI and keep account timing intact.

Other product surfaces can now consume the same line-level motion boundary.

* test(ui): cover shared kinetic copy phases

The extracted transition needs direct coverage outside the account panel.

Assert both lines, phase classes, accessibility state, and timing indexes.

The shared animation contract can now change without silent markup drift.

* fix(ui): add kinetic copy easing fallback

Shared kinetic copy relied on an account-panel-only motion token.

Add the account easing curve as a local custom-property fallback.

Entering lines now animate when rendered outside the auth screen.

* test(ui): cover kinetic copy easing fallback

Shared copy tests did not exercise styling outside the account panel.

Load the real stylesheet and assert the entering animation includes a local fallback.

Removing the fallback now fails the focused component test.
* docs: 新增协作规范,经 1024XEngineer#334 全员确认生效

* ci: allow pull request warning comments

* Update CONTRIBUTING.md

* ci: consolidate triage workflows
* feat(character): 创建角色时用 LLM 从描述补名称

POST /characters 在 name 为空时,经 ai_engine 的 LangChain 起名器从 description 生成不超过 20 字的名称。用户填写的名称优先;LLM 失败则截断描述,都空则用「未命名角色」。

Refs 1024XEngineer#188

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(character): 起名器改为懒加载,避免 create_app 依赖 API key

CI 没有 AI_API_KEY 时,装配期创建 ChatOpenAI 会让所有走 create_app 的测试在 setup 失败。LangChain 客户端推迟到第一次起名再构建。

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(character): 起名用独立 chat 型号,幂等重试不再先打 LLM

create_chat_model 在空 key/空型号时先拒绝,避免 ChatOpenAI(model="")。
同 workflow_run_id 先查已有角色再起名。装配期仍不创建 ChatOpenAI。

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: minorcell <120795714+minorcell@users.noreply.github.com>
* feat(frontend): show credit balance in account menu

* test(frontend): cover default quota adapter

* feat(frontend): show quota details in account center

* fix(frontend): keep quota visible while menu closes

* test(frontend): cover quota fallback states
* feat(quota): 生成任务调度接入预付费冻结与扣减

提交时按任务类型冻结积分,成功实扣、失败解冻,积分不足则整单回滚。

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(playtest): 稳住行走切帧断言,避免 Frontend CI 误红

覆盖率下 waitFor 默认 1s 会把重试耗在整页 HTML 上,点了行走仍读到 idle 首帧。
改为先等 aria-pressed,再核对按 index 排序后的 walk-01。

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(quota): 结算用冻结快照,启动时对账孤儿任务

capture/release 改读 task:{id} 的 FROZEN 流水,不再用当前定价。
启动扫描仍冻结的 PENDING/RUNNING:PENDING 重入队,RUNNING 失败并解冻。

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(quota): 补全冻结结算与启动恢复分支覆盖

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(project): 有角色挂载时拒绝删除项目

删除项目前按 project_id 查询角色,仍有挂载则返回 400,避免留下无归属资产。

Fixes 1024XEngineer#348

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(frontend): 按 oxfmt 折行项目导出与删除错误提示

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(project): 用外键与行锁堵住删项目和建角色的竞态

Character.project_id 加 RESTRICT 外键,删除时把约束冲突映射为 400。
创建角色与删除项目都对同一项目行 FOR UPDATE,已有库在补约束前也不会并发插出无归属角色。

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…Engineer#346)

* fix(user): 落库接口改为 session-per-call,去掉 NotImplementedError stub

Fixes 1024XEngineer#204

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(auth): 覆盖 login/改密/重置/改昵称 API 调用链

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(workflow_run): PATCH 用乐观锁校验 version

无条件递增会让并发写互相覆盖。更新改为按客户端 version 条件写入,冲突返回 409。

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(openapi): 同步 WorkflowRunUpdate 的 version 字段

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(workflow_run): 删除升 version,PATCH 只写变更列

软删除与无操作 PATCH 都走同一套版本校验,避免并发 DELETE 被旧快照写回 active。

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Johnny Zhang <jingheng_zhang@outlook.com>
…er#312)

* feat(quick-start): add kinetic copy cycle

Quick Start needs reusable subtitle transitions across entry and generation states.

Add a page-local copy cycle with ordered exit and entrance motion.

Respect reduced-motion preferences while preventing overlapping text.

* feat(quick-start): continue creation in one conversation

The creation entry and run view currently feel like separate products.

Reshape the run as one flowing transcript with a persistent composer and result-focused states.

Reuse existing session calls while making completed destinations explicit.

* test(quick-start): cover conversation continuity

The redesigned flow must preserve every existing Quick Start capability.

Cover entry motion, generation states, selections, recovery, saving, and explicit destinations.

Keep runtime Mock routes excluded while protecting the real service boundary.

* docs(quick-start): add continuity screenshots

The visible redesign needs reviewable desktop and mobile evidence.

Capture the completed conversation with export and explicit destination controls.

Keep the screenshots alongside the repository existing visual evidence.

* fix(quick-start): use shared composer shadow

Frontend CI rejects private color values in authenticated product pages.

Replace the two inline RGB shadows with the existing semantic panel shadow.

Keep the visual depth while preserving the shared color-system boundary.
* docs(readme): present live product access

The repository landing page still describes internal MS2 and MS3 milestones.

Present the live registration entry, current capabilities, supported setup, and CI commands.

Visitors can evaluate Windup and contributors can start from current repository facts.

* docs(readme): align launch tagline

The README still uses the previous asset-delivery slogan.

Replace it with the tagline already used on the product page.

The repository and live page now share the same launch message.

* docs(readme): add character journey hero

The README header lacks the visual story used on the product page.

Add a transparent, README-cropped character journey illustration beneath the live status.

The repository landing now connects the brand promise to the asset progression.

* docs(readme): tighten character journey hero

The README hero left excessive transparent space between launch status and artwork.

Crop the existing character journey asset to a compact 1536 by 440 canvas.

The GitHub preview keeps the full narrative while reducing vertical whitespace.

* docs(readme): clarify registration availability

Make the public access status explicit for visitors.

Update the launch line to state that registration is now open.

Keep the README aligned with the current product announcement.

* docs(readme): add project status badges

The public README now represents a live product and should expose current health signals.

Add deployment, frontend CI, backend CI, coverage, and license badges in one centered row.

Visitors can inspect project status without displacing the primary product links.

* docs(readme): refine status badges

The badge row should communicate product access without redundant repository metadata.

Label the availability badge with windup.xin and remove the license badge.

The header now presents a shorter and more direct status summary.
* feat(quick-start): present style directions without role examples

The entry paired the style cards with two named role pills, which read as ready-made characters rather than prompt seeds.

Remove the shortcut list and its pill row so the entry offers style direction only.

The entry now asks for a description instead of implying a catalog of existing assets.

* feat(quick-start): tighten the entry composer to a single line

The entry composer had grown into a two-row textarea, standing taller than the single-line composer the conversation view hands off to.

Restore the single-line input and move the focused state onto a dedicated shadow token instead of leaving focus as a border-only cue.

Both composers now share one height, and focusing the entry reads as a change in elevation.

* test(quick-start): cover the single-line entry composer

The entry assertions still described the two-row textarea and the retired role-example shortcuts.

Retarget the input assertions and add cases for the single-line composer and for the shortcuts staying absent.

A silent return to either shape now fails the suite instead of drifting unnoticed.
* fix(frontend): 处理 WorkflowRun 乐观锁冲突

* fix(frontend): reconcile workflow asset conflicts

* test(frontend): cover workflow conflict reconciliation

* test(frontend): decouple conflict test from call count

* fix(frontend): preserve workflow setup draft during upload

* test(frontend): cover conflict cleanup paths

* fix(frontend): harden workflow conflict recovery

* test(frontend): cover workflow recovery failures

* refactor(frontend): simplify workflow editor orchestration

* test(frontend): cover late workflow session cleanup
attack.md 拆成 sweep/thrust/project/lunge × side/front 八节,去掉 crescent /
broad side 这类形状先验 —— 它们等于断言角色手握一件有宽面的长条物,喂法杖 /
空手 / 四足角色时模型会凭空补出那件东西(1024XEngineer#195 只清了装备名词,没清形状先验)。

build_attack_prompt 新增 archetype(默认 THRUST,SWEEP 是唯一要求持有长条物的
一支),ActionSpec 加同名字段并对非攻击动作拒收;母版姿态同步分四支。
构图约束抽到 _framing 由代码统一追加到五个动作,只写正向计数句。

Refs 1024XEngineer#310

Co-authored-by: johnnyzhang-eng <johnnyzhang-eng@users.noreply.github.com>
* feat(workspace): add invite reward hint

Users may not discover invite rewards from the account center without a contextual entry point.

Show a restrained account-adjacent hint once per login session, dismiss it on account-menu open or close, and expire it after fifteen seconds.

Clear the session marker on logout so the next login can discover the reward again.

* docs(workspace): add invite hint screenshots

Reviewers need stable visual evidence for the contextual invitation entry point.

Add desktop and mobile captures of the account-adjacent reward hint from the local workspace.

Keep both responsive states available for direct PR review.

* fix(workspace): bind invite hint to login session

Acceptance found that route changes could repeat the hint and non-header logout paths could suppress it on the next login.

Mark the hint as seen when shown, clear the marker for every guest transition, and link directly to the invite section.

Cover timeout, close, route changes, external logout, and responsive evidence format.

* fix(auth): clear session-scoped invite hint

Protected routes can unmount the header before a guest-state effect clears the invite hint marker.

Move session-scoped storage cleanup into the shared authentication teardown used by logout, expiry, password changes, and cross-tab logout.

Replace the header-only false positive with provider-level lifecycle coverage.

* test(auth): cover session marker cleanup

The session-scoped storage boundary must preserve unrelated preferences and tolerate unavailable browser storage.

Cover prefixed-key removal, null storage, and storage access failures directly.

Raise patch coverage for the lifecycle fix without broadening production behavior.

* Merge pull request #14 from huyanxius/fennoai/pr-363-remove-images-20260818

chore(workspace): remove invite hint screenshots

---------

Co-authored-by: fennoai[bot] <231223108+fennoai[bot]@users.noreply.github.com>
…gineer#358)

Quick Start built the character name from the first 32 characters of the prompt, but CharacterCreate.name accepts at most 20, so confirming a character template failed for any prompt longer than 20 characters.

Drop the name from the create payload and let the backend derive it from the description, matching what workflow-editor already does.

Long prompts now finish character creation, and the backend naming added in 1024XEngineer#329 becomes reachable from Quick Start.
Co-authored-by: huyanxius <178478063+huyanxius@users.noreply.github.com>
* feat(auth): 用邀请码重新开放注册

公开注册仍关闭自动建号,改为邀请码门槛,并补齐查看、轮换和补填邀请码接口。

Closes 1024XEngineer#355

* test: 补齐邀请码注册与兑换的失败路径覆盖

覆盖空邀请码、发码碰撞、兑换接口以及账号中心邀请码加载/生成失败。

* fix(account): 兑换邀请码后刷新积分余额和流水

兑换成功只改了提示,同页余额与流水会停在旧数据。

* revert(frontend): 把邀请码 UI 交还给账号中心与工作台 PR

邀请奖励页和工作台提示已分别由 1024XEngineer#3621024XEngineer#363 承担,本 PR 只保留后端接口与 OpenAPI。

* fix(quota): 轮换邀请码加行锁,重复兑换改为业务错误

冷启动不加兑换次数上限。并发轮换用 FOR UPDATE 串行化;invitee 唯一约束冲突返回已填写过邀请码。邀请双方奖励默认改为 200,与账号中心 PR 对齐。

* fix(auth): 邀请码校验对齐邀请链接字符集

注册不再返回「请填写邀请码」:前端从链接传入 invite_code,空码和非法字符统一为邀请码无效。

* feat(auth): 邀请码选填,验证码登录可建号

注册无邀请码只发注册赠送;有有效邀请码再发双方奖励。login-by-code 对未知邮箱建号。注册赠送默认改为 300,邀请奖励保持 200。

* feat(auth): 邀请码仅注册可用,改为 TTL 与只增不删

去掉登录后补填;邀请码 30 天过期,轮换插入新行并截断旧码有效期。

* feat(quota): 邀请人每日最多 3 次邀请奖励

未过期邀请码仍可继续被使用并写入关系;邀请人每个 UTC 日最多入账 600 分,超出只跳过邀请人奖励。

* chore(openapi): 按导出脚本排序 InviteCodeOut 字段
* feat(render3d): 三渲二接进编排

引擎契约与 server 编排的后半段。路线选择由 server 读 DB 决定:造型上有
model_3d_url 就调 CharacterGeneratorPort.generate_rendered,没有则照旧走 i2v。
三渲二不进 ROUTE_MATRIX —— 那张表的隐含前提是"路线由动作物理性质唯一决定",
而这条还取决于该造型有没有 3D 资产。

引擎侧新增 generate_rendered 而不另立 port,并补一项分区动量读数:整幅的
motion_scale 与死帧判据看不见"腿在迈、手臂僵成柱子",而那正是自动绑骨漏认
肢体的典型产物。

出帧台 provider 走函数内延迟 import,没装它时 i2v / 逐帧两条路线仍可用。

Refs 1024XEngineer#192 1024XEngineer#122 1024XEngineer#121

* test(quality): 分区动量用例移出三渲二文件

这两条只用 PIL 和 slicing.quality,与三渲二 provider 无关。留在
test_render3d_route_and_assets.py 里会被那个文件的整体 skip 一并带走,
于是本 PR 新增的这项读数在 provider 合入前一条断言都不跑。

* fix(render3d): 渲帧策略延迟到被请求时才装

1024XEngineer#241 合入后 rebase 撞出来的:它给装配表加了一条断言,要求每个 GenRoute 成员都被装配,
而本 PR 新增的 RENDER_3D 依赖 1024XEngineer#270 的 provider 模块——那个模块在这条分支上还不存在,
装配期就 import 会让本来走 i2v 的任务也起不来。

改成装配表里放一个惰性策略:真走到三渲二那条路线时才 import 出帧台依赖。
断言因此仍然成立(表里确实有这个键),而缺 provider 也不影响其余路线。

两个状态都验过:不带 1024XEngineer#270 时 496 passed / 2 skipped;把 1024XEngineer#270 的 provider 层铺进工作区
再跑,591 passed / 14 skipped,惰性策略能真的实例化出 RenderFrameStrategy。

Refs 1024XEngineer#192

* test(render3d): 桩随 _dispatch_after_commit 新签名

* feat(ai_engine): 母版预检增加四肢粘连与独立色块两条警告

* feat(server): 造型级 3D 资产的建造、放行与丢弃端点

* docs(render3d): 注释里的成本论证改为定性表述

---------

Co-authored-by: johnnyzhang-eng <johnnyzhang-eng@users.noreply.github.com>
Co-authored-by: Johnny Zhang <jingheng_zhang@outlook.com>
* fix(postprocess): 逐帧补偿一段动作内的单调尺度漂移

* fix(postprocess): 空帧不再让整段跳过尺度漂移补偿

守卫用 len(spans) == len(frames) 判齐,而 spans 已滤掉空帧,于是序列里只要有一帧
量不到本体,整段补偿就被跳过,其余帧静默留着 i2v 的推镜漂移。

空帧只是缺一个观测:改为把 None 一并传给 scale_drift,它按真实帧号拟合、空位系数
取 1.0,其余帧照常补偿。

* fix(postprocess): 推镜补偿只在高宽同比时触发,不误伤真实姿态

---------

Co-authored-by: Johnny Zhang <jingheng_zhang@outlook.com>
Co-authored-by: Johnny Zhang <jingheng_zhang@outlook.com>
@minorcell minorcell closed this Aug 18, 2026
@fennoai
fennoai Bot deleted the fennoai/fix-pr-issue-warning branch August 18, 2026 11:23
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.

5 participants