更新角色动作正式字段与迁移文档

记录正式图片序列数据契约、placement-only 布局边界和无运行时 fallback 约束。
补充四阶段迁移执行顺序、blocker 处理、批次哈希门禁和发布维护窗口。
同步项目记忆中的长期决策与排障经验。
This commit is contained in:
2026-08-04 10:44:08 +08:00
parent be63c54a84
commit 73848e5174
6 changed files with 98 additions and 10 deletions
@@ -5638,6 +5638,21 @@
- 微信边界:小程序客户端仍只上传 `wechatPhoneCode``platform-auth` 必须要求微信成功响应中的 `phoneNumber``countryCode``purePhoneNumber` 均存在且非空,但只使用后两项执行国家码校验和 E.164 构造。腾讯官方仅说明境外 `phoneNumber` 会带区号,并未承诺 E.164 格式,中国号码示例中它与纯号码相同,因此不得校验 `phoneNumber == +{countryCode}{purePhoneNumber}`。微信字段缺失时失败关闭,不能使用普通请求的 `86` 默认值。
- 数据边界:认证投影与 SpacetimeDB 的 `phone_number_e164` 保持不变,不新增国家码或纯号码列,也不需要 schema 迁移或 bindings 生成。
## 2026-07-28 编辑器媒体类型统一由 assetKind 判定
- 决策:`assetKind` 是编辑器资源和素材唯一权威媒体类别;不新增或返回 `mediaType``character-animation` 渲染为序列帧,`video` 渲染为视频,`audio/sound-effect/background-music` 渲染为音频,其余类别渲染为图片。前端内部可保留派生的 `CanvasMediaType` 选择渲染器,但不能把它作为后端事实。
- 持久化边界:`editor_project_resource``editor_asset` 表尾只保存 `image_sequence_frames_json``image_sequence_duration_ms`,默认均为 `None``editor_showcase_asset` 作为提交时冻结的审核与公开快照,同样在表尾保存这两个字段并从账号素材逐字段复制,旧行默认均为 `None`。正式帧对象和角色动作生成响应都不保存或返回 `frameIndex`,数组位置是唯一播放顺序;帧数取数组长度,FPS 由帧数和毫秒时长即时推导,不持久化 `frame_count``fps` 或通用 `duration_seconds`。后端处理抽帧和逐帧去背时仍保留内部 `frame_index`,仅用于乱序并发收口、OSS 命名、日志和错误定位;仓库内旧 helper 同步升级,不为它保留外部兼容字段。
- 时长口径:`image_sequence_duration_ms` 只表示角色图片序列完整播放一次的毫秒时长,与音频 / 视频生成请求中的 `durationSeconds` 完全分离。角色动作与视频生成响应仍可携带各自既有的请求 / 结果级 `frameCount/fps/durationSeconds`,音视频画布布局也可保留既有运行时秒数;不得为音频生成响应新增 `durationSeconds`,也不得把任何音视频秒数写入资源 / 素材或图片序列字段。前端播放器使用 `imageSequenceDurationMs / imageSequenceFrames.length`,Spine 导出时才换算秒数并推导 FPS。
- 写入与复用边界:`assetKind=character-animation` 必须在 SpacetimeDB storage/procedure 边界同时提供至少两帧有效数组与大于 0 的图片序列毫秒时长;其他类别不得携带图片序列字段。同项目同源同媒体资源只允许 `None → Some` 单调回填,非空冲突失败关闭,延迟重试的 `updated_at` 取请求时间与既有时间的较大值。
- 生成关联边界:角色动作生成响应同时返回已经持久化的最终 `resource` / `asset`;带项目上下文时前端结果图层必须直接使用 `resource.resourceId` 及其预览视频来源血缘,缺少 resource 直接失败。无项目放置也必须绑定响应中的正式账号素材,不能从响应 `frames` 构造无资产真相的本地动作结果。
- repair 幂等边界:legacy 音频 repair 不再派生或回填任何资源级通用时长,因此新列上线前已完成 repair 的重放继续按原字段精确匹配;图片序列字段在音频行上均为 `None`
- 布局边界:`editor_project_resource` 的正式字段是角色动作唯一媒体真相;动作 layout 只保存资源引用和 placement,不保存帧、时长、预览、生成输入、资源元数据或顶层 `mediaType`。前端仍可从 `assetKind` 派生内部 `CanvasMediaType`,但后端响应清洗不得把普通 layer 的旧 `mediaType` 传回前端,也不得递归删除 `generationInputs.references[*].mediaType`
- 迁移边界(2026-08-03 收口):存量 `generation_inputs_json.characterAnimation`、已证明动作行的 helper 顶层 `frames/previewVideoPath/frameCount/fps/durationSeconds``screenColorHex`、正式帧 `frameIndex`、误标预览 MP4 和动作 layout 副本,由 `normalize_editor_character_animation_metadata_and_return``asset → project-resource → showcase → canvas` 一次性规范化。顶层字段只有先证明动作身份后才解释;普通图片 / 视频任意 JSON 中的同名字段不动。正式/旧版冲突与无法唯一补建的本地动作资源形成 blocker;apply 必须绑定同批 dry-run SHA-256,结束后全量复核零匹配、零 blocker。迁移完成后删除 api-server、admin、Web 与 helper 的 action fallback。
- 新写入边界:`assetKind=character-animation``generationInputs` 若含旧运行字段或 `screenColorHex`,正式帧若含 `frameIndex`api-server 和 SpacetimeDB storage 均失败关闭;门禁只对角色动作生效,不误伤其它素材的任意 generation input JSON。
- 交互边界:角色动作素材下载导出完整序列 ZIP;点击、HTML5 拖放和指针拖放创建可移动、循环播放且可保存恢复的序列图层。`assetKind=character-animation` 但缺少有效帧时按损坏素材失败关闭,不回退首帧 PNG。
- 精选展示边界:`/creation` 精选卡片和预览弹窗继续扩展各自现有媒体 renderer,不复用或重构画布图层组件;卡片静止时只读取首帧,hover / focus 后才加载并播放完整序列,预览弹窗提供播放暂停。后台素材查询与精选审核继续共用 `AdminEditorAssetMedia`,列表只读首帧,预览弹窗才逐帧使用管理员换签。两端均按 `imageSequenceDurationMs / imageSequenceFrames.length` 切帧,下一帧未就绪时保留上一帧且禁止淡入;损坏动作不退回普通图片。
- 精选帧授权边界:公开换签只对当前已通过、已展示、返还完成且未删除的精选动作,按同 owner 的冻结帧 `assetObjectId` / `objectKey` 形成 exact grant;不从 `imageSrc` 或前缀推导。隐藏、拒绝、删除或快照损坏后逐帧授权随当前事务真相撤销,`read-url``read-bytes` 继续共用该判断。
## 2026-07-24 后台用户详情展示历史花费泥点
- 口径:`historicalConsumedPoints` 表示用户历史总消费,只累计 `profile_wallet_ledger.source_type = asset_operation_consume``amount_delta < 0` 的绝对值;`asset_operation_refund` 不冲减,充值退款追回、余额重置、赠送和退款 hold 均不计入。
@@ -3892,6 +3892,34 @@
- 处理:灰度页只能以 `/admin/api/feature-gates` 为数据源,固定目标列表只登记现役功能;新增或退役业务 target 只修改固定目标注册,不得让通用页面依赖业务列表接口。旧 `creation-entry:*` 目标、接口和页面保持退役。
- 验证:`adminRoutes` 必须包含 `gray-release`admin-web TypeScript/ESLint/Vitest 不得排除灰度页;页面测试必须断言只请求 feature-gates,并继续覆盖现役固定 target、直接 Gate Key 保存与新 target 状态重置。
- 关联:`apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx``apps/admin-web/src/app/adminRoutes.ts``server-rs/crates/api-server/src/modules/admin.rs``docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`
## 角色动作不能靠素材主图或通用生成输入恢复
- 现象:角色动作在整画布导出时正常,但从素材库单项下载只得到第一帧 PNG,拖回画布也成为普通静态图片。
- 原因:`editor_asset.image_src` 只指向首帧;若 worker 把完整帧集塞进 `generation_inputs_json`,素材 DTO、用户输入清洗或画布布局任一层丢字段,就会退化成 PNG。再增加一个 `mediaType` 只能掩盖结果字段没有落到正式资源的问题。
- 处理:worker 只把完整帧集与图片序列毫秒时长写入 `editor_project_resource` / `editor_asset``image_sequence_frames_json``image_sequence_duration_ms`;数组位置是唯一帧序,不保存 `frameIndex`,帧数和 FPS 均按需派生。`assetKind=character-animation` 决定序列渲染。素材映射、单项下载和拖回画布只读取这两个正式字段,项目 resource 在保存 / 刷新后继续作为主真相;动作 layout 只保留资源引用和 placement,不再复制正式媒体结果。账号素材提交精选审核时,`editor_showcase_asset` 必须冻结复制相同字段,公开 read model 只返回正式字段。外部 helper 只调用一次动作生成接口并直接使用响应 `resource` / `asset`
- 画布回填:角色动作会形成“原角色资源 → 预览视频资源 → 最终序列资源”的血缘链。生成响应必须返回已经持久化的最终 resource,前端图层直接使用其 `resourceId`;不能继续构造 `local-resource-character-animation-*`,否则 `appendCanvasLayersWithResources` 会再次创建重复资源。新图层的 `sourceResourceId` 同时使用最终 resource 的直接来源(预览视频 resource),不能继续沿用请求中的原角色 resource;否则结构化保存会在已生成并计费后因血缘不一致而拒绝。修复时只替换资源关联与血缘字段,不要顺带把动作图层显示尺寸从生成占位尺寸改成原始帧分辨率。
- 历史处理:不要再在 read mapper 增加 `generationInputs` / layout fallback。使用 migration operator procedure 按 `asset → project-resource → showcase → canvas` 迁移;只有动作身份已由 `assetKind`、正式字段、嵌套 `characterAnimation` 或权威对象证明后,才解释顶层 `frames/durationSeconds`,否则会把无关任意 JSON 误分类。正式与旧版结果冲突必须形成 blocker,不能任选一份;旧 `local-*` 动作 layer 只有唯一匹配账号动作素材时才能补建项目资源。普通 layer 顶层 `mediaType` 在迁移和响应清洗时删除,但嵌套生成参考的 `mediaType` 保留。
- 新写入与验证:动作 `generationInputs` 出现 `characterAnimation/frames/previewVideoPath/frameCount/fps/durationSeconds/screenColorHex`,或正式帧出现 `frameIndex`HTTP 与 storage 双层拒绝;其它 asset kind 的任意 JSON 不受该动作门禁影响。测试覆盖两种历史 JSON、无关顶层同名字段、正式/旧版相等与冲突、可选帧引用合并、screen color 和 frameIndex 清理、预览 MP4 重分类、幂等、blocker/hash apply、画布 placement 清理/资源补建、正式字段缺失失败关闭和 helper 单请求。
## 图片序列时长不要复用通用媒体秒数
- 现象:把角色动作、视频、音频和上传媒体都写进通用 `duration_seconds`,随后又尝试用持久化 `frame_count/fps/duration_seconds` 互相校验,造成取整口径、生成参数和实际播放时长彼此污染。
- 原因:角色动作需要的是一组图片完整播放一次的精确时长;视频 / 音频的 `durationSeconds` 是生成请求或临时运行态参数。帧数已经由数组长度唯一确定,FPS 也可按需要推导,无需维护三份可冲突真相。
- 处理:资源 / 素材只保存 `image_sequence_frames_json``image_sequence_duration_ms`,精选审核快照只冻结复制这两个正式字段。角色动作要求至少两帧且毫秒时长大于 0;播放器按 `时长毫秒 / 数组长度` 计算间隔,Spine 导出时再换算秒数并推导 FPS。音频 / 视频 `durationSeconds` 不映射到这两个字段。
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs``src/components/image-editor/ImageCanvasWorldView.tsx``src/components/image-editor/ImageCanvasExportModel.ts`
## 精选角色动作显示首帧还要检查前端 renderer 与逐帧授权
- 现象:精选接口已经返回 `imageSequenceFrames` 和正确的 5 / 6 秒成本,但创作主页或后台审核仍只显示首帧;接入播放器后又可能只有第一帧成功、后续帧换签返回 404。
- 原因:快照字段、展示 renderer 和私有对象授权是三道独立边界。公开 `imageSrc/objectKey` 只代表首帧,不能让前端自动获得完整帧集;顶层精选 exact grant 也不会自动覆盖其它帧对象。
- 处理:公开精选模型必须把 `assetKind=character-animation` 映射到序列 renderer,并携带完整帧与毫秒时长;后台素材查询和精选审核共同透传同一字段并复用 `AdminEditorAssetMedia`。公开授权在 SpacetimeDB 同一事务快照中只按有效精选动作的同 owner 逐帧 `assetObjectId/objectKey` 匹配,不能放宽 generated 前缀。列表未交互时只读首帧,打开或激活动作预览后才逐帧换签,避免再次制造换签突发。
- 验证:模型 / 组件测试覆盖 4 / 5 / 6 秒动作、损坏序列不回退 PNG、后台两页共用播放器和未激活列表不逐帧请求;SpacetimeDB 测试覆盖主对象、每帧对象、无关对象、跨 owner 与取消展示后的授权撤销。真实浏览器和端到端验收由人工单独执行,不把 unit / component 结果写成 E2E PASS。
## 可复用资源回填必须保持时间戳单调
- 现象:延迟重试携带比既有行更旧的调用方时间,回填图片序列字段时若无条件写入,会使 `updated_at` 倒退,导致基于时间戳的同步看不到更新或排序错误。
- 处理:同源图片序列字段只允许 `None → Some`,非空冲突失败关闭;发生回填时 `updated_at = max(existing.updated_at, request_timestamp)`。legacy 音频 repair 不派生资源级图片序列或通用时长,重放继续精确匹配。
## 历史钱包消费不能从最近流水或通用订单快照推算
- 现象:后台用户详情要展示累计花费时,直接复用只返回最近 50 条的 `list_profile_wallet_ledger`,或在充值订单每行使用的通用钱包快照里扫描该用户全部流水。
@@ -163,7 +163,8 @@
- 上传按钮和拖拽上传都支持多文件;底部工具栏的上传入口选择文件后直接进入“上传素材”并在当前画布视口中心创建画布图层,素材栏文件夹内的上传入口只写入对应素材文件夹、不自动入画布;拖到文件夹或该文件夹内素材时进入目标文件夹;拖到画布时进入“上传素材”并在投放点创建画布图层。上传图片必须在创建占位素材、画布图层和账号级素材记录前先读取原图 Resolution,图层宽高、`originalWidth/originalHeight` 和素材库 `width/height` 都使用图片本身尺寸;上传视频同样在创建素材和图层前读取视频 metadata 宽高,保证单层下载或 ZIP 导出的真实视频文件重新导入后仍按文件自身尺寸入画布;仅在无法解析尺寸时才使用对应媒体兜底尺寸。
- 音频 / 视频素材卡和画布媒体图层必须提供稳定的非文字视觉预览:优先使用 `thumbnailSrc` / 视频 `poster`,没有真实首帧或音频封面时使用由媒体类型、素材名和地址派生的确定性视觉底图。视频图层使用原生 `<video controls preload="metadata" playsInline>` 播放,外层图层仍承接选择和拖拽语义;音频图层播放前继续通过 `/api/assets/read-url` 换签。画布素材导出按 `mediaType` 保留真实媒体格式:图片进入 `images/`,音频 / 视频进入 `media/`,文件扩展名从响应 MIME、`objectKey` 或源 URL 推断,不得把音频 / 视频导出成 PNG。
- 画布素材 ZIP 的 `metadata.json` 只保存前端信息弹窗和导出文件列表可见的展示快照:项目标题、导出时间、图层标题、文件路径、类型、生成输入、模型显示名、Task 短 ID、Object 显示值、Resolution / 时长和导出错误。导出的生成输入只保留用户实际填写或选择的内容;系统默认兜底提示词、固定工作流提示词、内置图标描述、UI 提取素材固定提示词等内置提示词即使存在于历史 `generationInputs`,也不得写入导出元数据。不得把 `projectId``layerId``resourceId``sourceAssetId``sourceResourceId`、原始 `prompt` / `actualPrompt` / `provider` 或画布坐标、锁定、隐藏等布局状态写入导出元数据;Object 字段仅沿用信息弹窗当前可见值。
- `生成角色动作` 的完成结果按序列帧素材处理:图层主 `src` 使用 `frames[0].imageSrc``mediaType` 固定为 `image-sequence``assetKind` 固定为 `character-animation`,完整帧列表写入 `imageSequenceFrames``previewVideoPath`保留为上游预览视频来源,不作为画布主媒体。下载和 ZIP 导出必须因此得到序列帧 ZIP / frames 目录,不能回退为预览视频或首帧 PNG;后端抽帧后逐帧保存带绿幕源图,上传时消费并释放原帧字节,再用该对象的 600 秒 OSS URL 完成透明化并落盘透明帧素材。
- `生成角色动作` 的完成结果按序列帧素材处理:`assetKind` 固定为 `character-animation`,图层主 `src` 使用首帧,前端内部据 `assetKind` 派生 `CanvasMediaType="image-sequence"` 选择播放器。`previewVideoPath`为上游来源,不作为画布主媒体。下载和 ZIP 导出必须得到序列帧 ZIP / frames 目录,不能回退为预览视频或首帧 PNG;后端抽帧后逐帧保存带绿幕源图,上传时消费并释放原帧字节,再用该对象的 600 秒 OSS URL 完成透明化并落盘透明帧素材。
- 素材库和项目资源 API 不返回独立 `mediaType`;前端只按 `assetKind` 映射渲染器:`character-animation → image-sequence``video → video``audio/sound-effect/background-music → audio`、其余 → image,不按扩展名猜类型。最终动作 asset / resource 平铺返回 `imageSequenceFrames/imageSequenceDurationMs`;正式帧对象不含 `frameIndex`,数组位置是唯一播放顺序,帧数取数组长度,FPS 按帧数与毫秒时长即时推导。只有至少两帧且播放时长大于 0 的角色动作素材才能进入序列播放器,否则按损坏素材失败关闭。预览视频作为独立 `assetKind=video` 资源保存,最终动作以 `sourceResourceId` 保留来源关系,不重复保存路径。从素材库点击、HTML5 拖放或指针拖放到画布时必须复制完整字段并先创建正式 project resource;新 layout 只保存 resource 引用和几何,不保存帧集或预览视频,刷新后从 project resource 恢复循环播放。历史 `local-*` 自包含动作布局仍可读取。
- 素材面板支持按素材名、文件夹名、生成信息、模型、任务和媒体类型搜索,并支持选择模式框选,一次选中多个素材,并可批量移动或删除上传素材。
- 图层面板支持按图层名、生成信息、模型、任务和媒体类型搜索;支持选择多个图层后创建图层组,组名和 groupId 随画布布局保存。
- 小地图支持拖拽视口框,拖动时画布 viewport 跟随移动;`pointermove` 更新必须通过 `requestAnimationFrame` 合帧,结束拖拽时 flush 最后一帧,避免高频 pointermove 直接压垮 React 渲染和项目持久化链路。
File diff suppressed because one or more lines are too long
@@ -88,6 +88,8 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去
外部生成任务摘要投影与历史 payload 维护使用 `npm run spacetime:external-generation:maintain -- ...`,且只能由已授权 migration operator 的 SpacetimeDB CLI 登录态执行。脚本默认 dry-run、每次只处理一批,绝不自动循环全表;`--apply` 才写入。先发布包含 `external_generation_job_summary` 与 cursor 索引的 SpacetimeDB 模块,在维护模式内对事故时间以前的编辑器终态任务执行小批 dry-run,例如 `npm run spacetime:external-generation:maintain -- --database <database> --server-url <url> --limit 5 --completed-before-micros <micros>`;核对 `matched_count``before_bytes``after_bytes``inline_media_count` 后,保持本批输入 cursor 不变并追加 `--apply` 重跑同一批,即使最后一批 `has_more = false`,只要 dry-run 仍有 `matched_count` / `selected_count` 也必须 apply;只有 apply 成功后才使用它返回的 `next_cursor_job_id` 继续。B-tree cursor 的选择阶段最多反序列化 `limit + 1` 行,apply 会再按主键逐条读取选中行但不会同时保留整批 payload;如怀疑存在单行异常巨型历史 JSON,先用 `--limit 1`。payload 压缩硬限制 `source_module = editor-canvas`;终态压缩完成后,用 `--backfill-summaries` 先 dry-run、再 `--apply` 分批补齐仍缺失的活动任务或无内联媒体历史任务摘要,直到 `has_more = false`,最后再切换使用 summary procedure 的 api-server。Stdb 构建 artifact 和完整 release 包都必须包含 `scripts/spacetime-maintain-external-generation-jobs.mjs``scripts/spacetime-migration-common.mjs`。首次上线不得让 Full Build 从 Stdb 自动直落 API`STDB_API_ROLLOUT_MODE` 默认 fail-closed 为 `pause-after-stdb`,必须填写受限的 `STDB_API_ROLLOUT_APPROVERS`Stdb Publish 通过 `KEEP_MAINTENANCE_MODE` 保持维护文件并停止旧 API/controller/worker,暂停点最多等待 4 小时,完成上述维护并确认无后续批次后才由指定审批人放行 API。定时构建缺少审批人时必须在发布前失败,不能静默退回 `normal`;也可分开运行 Stdb publish、维护、API deploy 三个受控 Job。任一批次都不得处理 pending / running payload;不要用 runtime writer、bootstrap secret 或匿名 identity 代替 migration operator,也不要在未核对 dry-run 时直接 apply。
角色动作正式字段收口使用 `node scripts/spacetime-normalize-editor-character-actions.mjs --database <database> --server-url <url>`,且同样只能由已授权 migration operator 执行。必须先发布包含 normalization cursor 索引和 `normalize_editor_character_animation_metadata_and_return` 的 SpacetimeDB 模块,在 API / worker 仍处于维护模式时先运行默认全量 dry-run;脚本固定按 `asset → project-resource → showcase → canvas` 扫描,普通 scope 每批最多 25 行,canvas 每批最多 5 行。出现 blocker 时先按输出 ID 人工核对正式/旧版冲突或缺失资源,不得跳过 scope;确认 dry-run 后追加 `--apply`,脚本会对每批重新 dry-run、携带该批 SHA-256 apply,并在最后从头要求四个 scope 均为零匹配、零 blocker。只有该复核通过后才发布移除 action fallback 的 API / Web。Stdb build artifact 和完整 release 包必须同时包含 `scripts/spacetime-normalize-editor-character-actions.mjs``scripts/spacetime-migration-common.mjs`
自 2026-07-11 起,`Genarrative-Full-Build-And-Deploy` 的每日 04:00 timer 默认以 `DEPLOY_TARGET=development``STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate。三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,不得依赖下游 Job 默认值或提前各自发布;统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。上文“定时构建缺少审批人时失败”的旧口径不再作为当前 dev 定时发布行为。
Full Job 通过 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION` 明确选择完整发布成功后是否退出维护,默认勾选以保持历史行为。Full 对 Stdb Publish 和 API Deploy 两个下游阶段都固定传 `KEEP_MAINTENANCE_MODE=true`,让 maintenance marker 持续覆盖 Stdb → API → Web 整段发布;Web Deploy 成功后才进入独立 `Exit Maintenance` 阶段。该阶段只能通过 `agent none` 和显式 `node(...)` 分配目标机,直接执行 `/opt/genarrative/current/scripts/deploy/maintenance-off.sh`;目标机不得 checkout Git、挂载 Git SSH 凭据或依赖 Jenkins workspace 源码。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。`Genarrative-Api-Deploy` 也单独暴露 `KEEP_MAINTENANCE_MODE` 参数,并转换为随发布包脚本的 `--keep-maintenance-mode`;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。外部生成 queue 的 `warning` 由 API/worker 固化为可直接展示的完整文案,Web 不再补前缀,因此 API/worker 与 Web 必须在同一维护窗口按同一版本协调发布;分开运行 Job 时先保持维护态完成 API/worker,再发布 Web,二者完成后才能恢复公网,不得在公网可用期间只滚动其中一侧。
@@ -585,7 +587,7 @@ Pingora current release 自审脚本 `scripts/ops/pingora-current-release-audit.
同一 API release 随包依赖还必须包含 `scripts/check-pingora-release-readiness.mjs``scripts/check-pingora-canary-live.mjs`。前者在 current release 上以 `--release-runtime-only` 汇总运行时复核,后者支撑目标 Nginx canary live smoke;缺少任一脚本时不能进入直连切换窗口。
`Genarrative-Stdb-Module-Build` 的 Jenkins 归档产物必须包含 `build/<version>/spacetime_module.wasm``spacetime_module.wasm.sha256``release-manifest.json``scripts/deploy/production-stdb-publish.sh``scripts/deploy/production-runtime-writer-identity-rotate.mjs``scripts/deploy/maintenance-on.sh``scripts/deploy/maintenance-off.sh``scripts/spacetime-migration-common.mjs``scripts/spacetime-maintain-external-generation-jobs.mjs``scripts/spacetime-migrate-editor-canvas-layout.mjs``scripts/database-backup-to-oss.mjs`,不得包含 `migration-bootstrap-secret.txt` 或任何原始 bootstrap secret。`Genarrative-Stdb-Module-Build` 只接受 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256` 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb `release-manifest.json``migration_bootstrap_secret_sha256` 记录该非敏感摘要。`Genarrative-Stdb-Module-Publish` 只通过 `copyArtifacts` 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret Filepublish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 `migration_bootstrap_secret_sha256` 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secretID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
`Genarrative-Stdb-Module-Build` 的 Jenkins 归档产物必须包含 `build/<version>/spacetime_module.wasm``spacetime_module.wasm.sha256``release-manifest.json``scripts/deploy/production-stdb-publish.sh``scripts/deploy/production-runtime-writer-identity-rotate.mjs``scripts/deploy/maintenance-on.sh``scripts/deploy/maintenance-off.sh``scripts/spacetime-migration-common.mjs``scripts/spacetime-maintain-external-generation-jobs.mjs``scripts/spacetime-normalize-editor-character-actions.mjs``scripts/spacetime-migrate-editor-canvas-layout.mjs``scripts/database-backup-to-oss.mjs`,不得包含 `migration-bootstrap-secret.txt` 或任何原始 bootstrap secret。`Genarrative-Stdb-Module-Build` 只接受 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256` 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb `release-manifest.json``migration_bootstrap_secret_sha256` 记录该非敏感摘要。`Genarrative-Stdb-Module-Publish` 只通过 `copyArtifacts` 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret Filepublish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 `migration_bootstrap_secret_sha256` 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secretID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
三个 SCM Jenkinsfile 将 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 默认固定为 `genarrative-spacetime-bootstrap-secret-dev-file`。Secret File 的原文只存在于 Jenkins Credentialscredential ID、参数默认值和定时 / 发布行为以仓库 Jenkinsfile 为事实源,不能只改 Job UI,因为 Declarative Pipeline 下一次载入会重写参数定义。旧 Secret Text `genarrative-spacetime-bootstrap-secret-dev` 继续保留给 Database Import / Export,不得原地改类型或删除。
@@ -181,9 +181,11 @@
- 后端先计算整批精确采样时刻,再用单个 FFmpeg filter graph 统一解码预览视频并输出 `32 / 40 / 48` 张源帧;不得为每帧重新启动 FFmpeg、重复解码同一视频,也不得用会改变现有尾帧安全时刻的粗粒度 `fps` 抽帧替代。批量命令成功后必须逐一确认全部目标帧文件存在,缺少任一帧都按整批失败处理并保留缺帧编号、目标时刻和输出路径诊断。
- 每帧绿幕源图字节由上传 owned 消费(`frame.bytes` 移入 put,上传完成后释放原帧缓冲,不克隆保留);后续父流程只持 object key,并为每帧向唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,不直接签发 BgFilter URL、不直连 provider,也不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。每帧请求分别携带按父剩余绝对预算派生的 `maxQueueWaitMs` 和公式化 `callBudgetMs`;子 worker 在默认 `Q=2048` admission 保险丝和 `Semaphore(N=16)` 约束下排队,取得 provider permit 后才启动调用预算,每次 provider attempt 前重新签发 600 秒 GET URLmultipart 仅传 `image_url`(加 `background_mode=flat``seg_model=birefnet``cross_check=on` 与同一次生成已选定的 `screenColor`),不传 `file`。真实 provider attempt 按 `N × est × 2` 派生,调用预算按 `2 × attempt + 1s` 派生;冻结 `est=5000ms` 时分别为 `160s / 321s`,排队不侵蚀最多两次顺序 attempt 的完整窗口,`32 / 40 / 48` 帧也不再增加单帧 attempt。成功图片由子 worker 以内部 HTTP 二进制 body 返回父流程,不在子侧落 OSS;BgFilter 最终失败且父业务预算仍有效时,由父流程进入 `阿里云通用抠图(按签名 URL 单独下载)→ 本地 editor_green_screen(再按 object key 独立下载一次并在产出后释放)`
- BgFilter、阿里云或本地键色返回透明结果后,父流程继续通过现有最终帧 finalizer 转为 RGBA8,按宽高比居中放入最终目标尺寸,并使用 `RGBA(0,0,0,0)` 补边;最终帧 OSS 与业务写回仍由父流程完成。上述样例最终输出必须为 `323×480 RGBA8 PNG`,顶部和底部各 `23px` 透明 padding,内容区域完整保留抠图结果。
- 全部 `32 / 40 / 48` 帧以覆盖本次所有帧的无序在途集合向内部 worker 提交,允许乱序完成并最终按 `frameIndex` 排序;BgFilter provider 的实际在途请求受唯一 worker 的 `N / Q` 限制。任一帧最终失败时先排空全部已启动 Future,再让整项任务失败退款,不发布缺帧动画。
- 全部 `32 / 40 / 48` 帧以覆盖本次所有帧的无序在途集合向内部 worker 提交,允许乱序完成并最终按后端内部 `frame_index` 排序;该索引只服务并发收口、OSS 命名、日志和错误定位,不写入正式 asset / resource 帧对象。BgFilter provider 的实际在途请求受唯一 worker 的 `N / Q` 限制。任一帧最终失败时先排空全部已启动 Future,再让整项任务失败退款,不发布缺帧动画。
- 抽帧结果写入 OSS,并返回帧路径、帧尺寸、帧数、fps、预览视频路径、模型、价格和实际 prompt。
- 画板前端回填角色动作结果时,必须以 `frames[0].imageSrc` 创建 `mediaType: "image-sequence"``assetKind: "character-animation"` 图层,并把完整 `frames` 保存为图层 `imageSequenceFrames``previewVideoPath` 只保留为上游预览视频来源,不作为画布主媒体
- 角色动作图层在画布中使用序列帧播放器循环展示透明 PNG 帧;刷新恢复时必须继续读取 `imageSequenceFrames`,不能回退到 `<video>` 预览
- 画板前端回填角色动作结果时,以正式 resource / asset 的 `assetKind: "character-animation"` 选择序列帧播放器并以首帧作为图层主图;带项目上下文的生成必须返回正式 project resource,图层直接使用其 `resourceId`。无项目放置只允许绑定响应中的正式账号素材,不从生成结果 `frames` 构造自包含 `local-resource-character-animation-*`。预览视频作为独立 `assetKind: "video"` 资源保存,但不复制进最终动作 layout
- 角色动作图层在画布中循环展示透明 PNG 帧;刷新恢复只从 project resource 的 `imageSequenceFrames/imageSequenceDurationMs` 读取正式结果。动作 layer layout 只保存 `layerId/resourceId` 和几何、层级、可见性等放置信息,不保存 `mediaType`、动作帧、时长、预览、生成输入或资源元数据副本;资源缺字段时按损坏数据失败关闭
- 点击角色动作图层下载时,必须打包下载序列帧 ZIP;画布素材 ZIP 导出时,角色动作写入 `sequences/<编号-标题>/frames/`,而不是导出预览视频。
- 角色动作生成 BFF 在同一请求内保存两类账号素材:上游预览 MP4 使用 `assetKind=video` 并承载生成成本,最终透明帧集使用 `assetKind=character-animation` 且派生成本为 0;响应中的 `resource` / `asset` 固定指向最终透明帧集对应的项目资源与账号素材。调用方必须直接使用它们,不得再以第一帧调用资源或素材创建接口。
- 最终项目资源和账号素材只把 `imageSequenceFrames/imageSequenceDurationMs` 写入正式媒体字段,并以 `sourceResourceId` 指向独立预览视频资源;正式帧对象和角色动作生成响应都不含 `frameIndex`,数组位置是唯一播放顺序,帧数和 FPS 均按需派生。`generationInputs` 只保存 `fields/references` 等用户可重放输入,`screenColorHex` 和动作运行结果均不落入其中。存量 `characterAnimation`、已确认动作行的 helper 顶层字段、`frameIndex`、误标预览 MP4 及动作 layout 副本由受 migration operator 限制的四阶段数据库 procedure 一次性规范化;上线完成后 api-server、后台、Web 和外部 helper 不再读取 legacy fallback。新动作写入若提交旧运行字段或 `frameIndex`HTTP 与 SpacetimeDB storage 均明确拒绝。
- `frames[0].imageSrc` 仍作为后续动作素材快速编辑或再次生成动作时的透明帧来源。该规则不得跳过后端原有的视频生成、抽帧、透明化处理和帧素材落盘流程。