feat:backend dev - #75
Conversation
Describe the backend package layout (common/framework/app) and the server domain module split: user, project, asset, character with action/character_template/wearable subdomains, generation and media. All modules currently define abstract interfaces only.
Add the shared response/exception kernel in windup_common: Response/ListResponse unified bodies (HTTP always 200, business code in body), BizException base, ModelException for LLM calls, and the BizCode/ModelErrorType enums. Add the app-level global exception handlers that convert these into Response.fail.
Add windup_framework infrastructure abstractions: SQLAlchemy db base/session, config loaders (database/provider/storage), LLM provider protocols (chat/image/video) and Kodo object storage.
Add abstract service interfaces and domain models for the server modules: user, project, asset, character (with action/character_template/wearable subdomains), generation and media. Drop the now-obsolete .gitkeep placeholders.
Drop the placeholder health router and its registration in create_app; the web layer now revolves around the global exception handlers.
Expose the windup CLI entrypoint (windup_app.bootstrap.app:main) and add python-multipart for form/file uploads; refresh uv.lock accordingly.
Point the sub-entity references at the nested character subpackages (character.action / character_template / wearable) instead of the old top-level paths.
Remove unused imports flagged by ruff F401 in generation/interface.py and user/model.py.
将 validate-branch 与 validate-commits 从 backend.yml 拆到独立的 naming.yml。命名规范门禁不局限于 backend 范畴,应作为仓库级 CI 单独维护(PR review:@minorcell 建议)。 backend.yml 仅保留 lint-and-test;naming.yml 用独立 concurrency group 避免与 backend 共用 group 互相取消。
Add SqlAlchemyProjectService inheriting ProjectService; implements create/get/list/delete plus project_name_exists against SQLAlchemy. - interface: reconcile ProjectService to session-per-call; session is passed per request via get_session, create_project takes **fields. - service: stateless impl as a module-level `service` singleton; flush only, commit/rollback handled by get_session; list ordered by id desc. - model: id uses BigInteger().with_variant(Integer, "sqlite") so the in-memory SQLite test DB autoincrements; Postgres unchanged. - api/bootstrap: import service from the service module, register project_router, add a print_banner startup stub.
Add SQLite in-memory test fixtures and project module tests. - conftest: SQLite in-memory fixtures (StaticPool, single connection) with no Docker Postgres dependency; get_session override for the API TestClient. - test_project_service: 14 unit tests for create/exists/get/list/delete covering the unique constraint, pagination, id-desc ordering and user filter. - test_project_api: 9 end-to-end tests via TestClient covering CRUD and the unified response contract (HTTP 200, code in body, 400/404).
Reflect character/media/generation/user module redesigns in the split document.
Replace strategy pattern with typed submit methods.
- model: add CharacterImageOutput/CharacterActionOutput/CharacterActionFrame
as typed task results, remove generic GenerationResult
- interface: split submit into generate_character_image and
generate_character_action with typed inputs
- document SSE streaming flow (GET /generation/tasks/{id}/stream)
replacing frontend polling
Remove OAuth abstract methods and models from UserService. Commented-out methods: get_oauth_authorize_url, login_by_oauth, bind_oauth, get_oauth_bindings. Related imports OAuthCallbackInput and UserOAuth removed from interface. The OAuth region is preserved as a placeholder for future extension.
Add ObjectStorageMediaService implementing MediaService. - service: upload to Kodo via KodoStorage adapter, auto-generated object keys with category prefix, no user filenames in keys - app: register media router (POST /media/upload) in create_app
refactor:asset character media
ci: split naming gate into its own workflow
feat:project module
跨层契约(common,无内部依赖):ActionType/GenRoute/CharacterCard/ActionSpec/AssetPackageRef。ai_engine 与 app 均依赖此。Refs 1024XEngineer#53。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SufyVideoProvider(kling i2v,OpenAI 兼容异步 /videos)+ OnnxU2NetMatteProvider(onnxruntime 直跑 u2netp,替代 rembg 在 3.12 无轮子的 numba 链)+ 能力接口(Image/Video/MatteProvider)。同时声明 framework 代码实际 import 的依赖(langchain-openai/onnxruntime/qiniu/pillow/numpy)——补齐 1024XEngineer#64 机器审指出的缺依赖。Refs 1024XEngineer#53 1024XEngineer#35。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
填充空骨架:slicing(抽帧 + 周期/一次性选帧)、strategy(ROUTE_MATRIX 分流 + VideoFrameStrategy 图生视频)、prompt(walk/jump/attack/idle 提示词库)、postprocess(像素化 + 脚线对齐 + 打包 + 逐帧时长)、generator 串联。MVP:root_motion 暂不做(character_data.frames 无该字段,见 1024XEngineer#63);graph/ 留占位非本 PR。Refs 1024XEngineer#53 1024XEngineer#35 #21。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove dead storage module (empty __init__.py and kodo.py) from framework - Implement actual Qiniu Kodo SDK upload in ObjectStorageMediaService - Move MediaCategory to windup_common.enums.media, remove duplicate from media/model - Update media __init__ to import MediaCategory from common
Move qiniu import inside upload() so module collection does not fail when qiniu is not installed (e.g. CI test runs).
按 MVP 边界(与作者对齐):CharacterGeneratorPort.generate(card, action, master, progress) → GeneratedAction(frames/durations/fps);母版由 server 以 bytes 传入,不再经 ArtifactStore;去掉 sprite sheet 打包与存储引用(上传/落库/导出在 server/export 侧)。补 VideoFrameStrategy.derive 离线端到端测试(monkeypatch 抽帧,不解码 mp4/不联网)。Refs 1024XEngineer#53。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refactor(media): implement Qiniu upload and deduplicate MediaCategory
feat(generation): add task persistence layer and service skeleton
实现作者留的 generate_character_action 空桩。为满足'入口层不经 ai_engine 直连'门禁(连间接链 web→service→ai_engine 都禁),拆分:service.py=提交+查询(web 依赖,不碰 ai_engine),新 executor.py=编排(调 ai_engine);bootstrap 把 run_action_task 注入 app.state,web 运行期从 request.app.state 取、经 BackgroundTasks 调度并传 project_id,不静态 import ai_engine。编排链:mark_running → 取 project 约束(character_perspective→facing,朝向须对齐母版 1024XEngineer#35) → CharacterGenerator.generate → 逐帧 media 上传 → CharacterActionOutput → COMPLETED/FAILED 兜底。顺带清理 merge 带入的 F401。Refs 1024XEngineer#53 1024XEngineer#35。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
内存 sqlite + 真实 GenerationTaskRecord ORM + 真实 CharacterGenerator(抽帧 monkeypatch,不联网、不碰对象存储):① 提交→后台跑→出帧→上传→COMPLETED;② project perspective 约束 facing 喂进 ai_engine;③ 出错兜底 FAILED。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(generation): 动作生成编排接入 ai_engine(executor + 项目约束)
feat(ai_engine): 视频路线角色动作生成管线(接入 backend-dev)
feat:generation service
① SufyImageProvider 图生图落地(gemini-flash-image,OpenAI 兼容 /chat/completions)。② ImageTaskExecutor:参考图+prompt→图→上传→回写 image_url。③ 项目全局约束 ProjectConstraints 统合进图/动作生成:character_perspective(1横版side/2俯视/3·2.5D front)→朝向+视角词;game_style→画风词/是否像素化;sprite_width×height→动作帧按尺寸切帧;directional_movement(1/4/8)→方向数(MVP出主方向,多方向待扩展)。角色图与动作均从 project 取约束、与项目关联。Refs 1024XEngineer#53 1024XEngineer#63。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bootstrap 挂载 character/media 路由并注入 run_action_task/run_image_task 到 app.state;web 端用后台线程调度(BackgroundTasks 对同步端点不触发),经 request.app.state 取执行器、不静态依赖 ai_engine(满足入口层门禁);image/action 端点透传 project_id 供约束。character/service.py+web/api/character.py 取自 backend-dev #17(逐字节相同,使本分支可端到端运行,合并 backend-dev 时不冲突)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
onnxruntime 自 1.24 起不再发布 macOS Intel(x86_64)wheel,导致 Intel Mac 装不上(抠图 MatteProvider 依赖它)。钉 <1.24 解析到 1.23.2——仍覆盖 Intel/arm64/Linux + py3.12,API 与新版一致、抠图代码零改动、不影响生成管线。换 rembg 无效(其底层同样依赖 onnxruntime,且 numba 老链在 3.12 无轮子)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…straints feat(generation): 角色图片生成 + 项目全局约束接入(HTTP 全流程)
fix(framework): 钉 onnxruntime<1.24 以支持 macOS Intel(x86_64)
…e optional on macOS x86_64
…d type field in result
backend-dev 的 ActionType.JUMP 值是大写 "JUMP",与其它小写成员不一致,且 executor 按值映射到 ai_engine 的 JUMP="jump" 会失败;改为 "jump"。(原 #24 另加一个 JUMP="jump" 导致枚举'JUMP already defined'、pytest 挂——改成修正现有值,不重复定义。)另 ROUTE_MATRIX 把 idle 从空桩 PROC_IDLE 改走 VIDEO_I2V(build_idle_prompt 躯干呼吸)。现 walk/idle/attack/jump 四动作全走视频 i2v。Refs 1024XEngineer#53 1024XEngineer#35。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… sprite_sample_url
feat(generation): 启用 jump 动作 + idle 走 i2v(四动作全跑通)
idle: 提示词原写 "chest and shoulders rise and fall",直接让肩膀起伏, i2v 出的是耸肩不是呼吸。改为只让胸腔/躯干在慢节律里起伏、肩膀锁平同高, 并去掉亚阈值幅度词(barely/faintly),过 prompt_lint。 jump: 重甲带剑角色的跳跃强动作先验压不住——站立母版让 i2v 只举剑摆造型、 不腾空(同 attack 用站立母版会抡过头顶的老坑)。按 master_prep 既有铁律 "母版姿态决定动作,提示词只能微调",给 jump 在 MASTER_POSES 补极限蓄力 半蹲母版姿态;runtime 的 prepare_master 仍只补顶部留白,此项为纯 spec。 prompt_lint 通过。on-screen 效果复验待 qnaigc 视频源站从 522 恢复后补跑。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(ai_engine): idle 去耸肩、jump 补蓄力母版压站姿先验
There was a problem hiding this comment.
Summary
The current head has several backend correctness and security blockers. Inline comments cover task dispatch, project scoping, outbound URL fetching, and upload resource handling.
Additional Finding
- [P1] Add database migrations before deployment. The PR introduces
windup_project,windup_character, andwindup_generation_taskORM tables, but schema creation only appears in tests viaBase.metadata.create_all; the application has neither migrations nor startup schema creation. A fresh or existing deployment will fail with missing-table errors when these endpoints are called. Please add versioned migrations and document how they are applied.
Verification
GitHub reports lint-and-test, Branch name, and Commit messages successful for head dbbcb74723b722ae0f992aa056be0bd7af894533. I could not rerun the commands locally because uv is not installed in the review environment.
| task = generation_service.generate_character_image( | ||
| session, user_id=body.user_id, project_id=body.project_id, input=input_data, | ||
| ) | ||
| threading.Thread( |
There was a problem hiding this comment.
[P1] Dispatch only after the task transaction commits. create_task() only flushes, while get_session() commits during dependency teardown after the handler returns. This new thread can therefore open its session before the task row is visible; update_status()/update_result() silently no-op for a missing row, leaving the eventually committed task permanently PENDING while generation and uploads still run. The action endpoint has the same race. Use a durable queue/outbox or otherwise enqueue after a successful commit; this also avoids creating an unbounded daemon thread per request.
| def get_task( | ||
| self, session: Session, project_id: int, task_id: int, | ||
| ) -> GenerationTask | None: | ||
| return task_repo.get_task(session, task_id) |
There was a problem hiding this comment.
[P1] Enforce the supplied project scope. project_id is ignored here, so GET /generation/tasks/{task_id}?project_id=<anything> returns any task with that ID, including its input, generated URLs, and error details from another project. Query by both task_id and project_id (and the authenticated user once auth is wired).
| def _download_master(self, input: CharacterActionInput) -> bytes: | ||
| if not input.reference_image_urls: | ||
| raise ValueError("缺少母版:reference_image_urls 为空") | ||
| resp = httpx.get(input.reference_image_urls[0], timeout=30.0) |
There was a problem hiding this comment.
[P1] Do not fetch arbitrary request-provided URLs from the backend. reference_image_urls[0] comes directly from the generation request, so callers can make the server connect to loopback, private-network, or cloud-metadata endpoints (SSRF). The image-generation _download() path has the same issue. Prefer object-storage keys you control, or strictly validate scheme/host and resolved IPs (including redirects) before fetching.
| if not file.content_type or not file.content_type.startswith("image/"): | ||
| raise BizException("仅支持图片文件", code=BizCode.BAD_REQUEST) | ||
|
|
||
| data = await file.read() |
There was a problem hiding this comment.
[P1] Enforce a hard upload-size limit while reading. await file.read() buffers the entire multipart body with no ceiling, so a single request can exhaust worker memory and then incur an arbitrarily large object-storage upload. Stream in bounded chunks and reject once the configured maximum is exceeded; Content-Length can be an early check but must not be the only enforcement.
| size=len(data), | ||
| category=category, | ||
| ) | ||
| result = service.upload(data, metadata) |
There was a problem hiding this comment.
[P2] Keep the synchronous storage SDK off the event loop. This endpoint is async, but service.upload() performs blocking Qiniu network I/O, so each upload stalls the server's event loop and delays unrelated requests. Make the route synchronous or run the blocking call in a thread pool (ideally while streaming rather than after buffering).
该包职责是**生成任务的编排调度**(建 PENDING 记录 → 后台 executor 驱动 → 查询状态), 实际 AI 生成在 ai_engine;原名 generation 易与"生成本身"混淆,更名为 orchestrator。 - git mv server/generation → server/orchestrator(rename 保留历史) - 全部 windup_app.server.generation → .orchestrator(bootstrap / web api / 包内自引用 / 测试) - 分层契约不变(约束在 windup_app.server 层级,不牵子包名) 门禁:ruff / lint-imports(2 kept) / 编排测试(3 passed) 全绿。类名(GenerationTask 等) 与 web 路由不动(仍描述生成任务,改动最小)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n-orchestrator refactor(app): generation 包更名为 orchestrator(生成任务编排/调度)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
背景:
feat(backend): 后端核心模块实现 — 基础设施 + 生成管线 + API 契约
新增能力
common(共享层)
framework(基础设施层)
ai_engine(AI 能力层)
app(应用层)