优化抠图链路的OSS临时URL调用
手动去背景直接校验对象归属并向BgFilter提交OSS临时URL 角色动作帧上传OSS时转移字节所有权并按object key执行去背和降级 删除旧的BgFilter文件上传与阿里云字节输入适配链路 更新抠图内存边界、接口契约与运维文档
This commit is contained in:
@@ -18,15 +18,15 @@
|
||||
|
||||
## 2026-07-17 生成后抠图原图以 OSS 作为内存生命周期边界
|
||||
|
||||
- 背景:角色形象、图标图集和 UI 素材图集的生成原图虽然已先落私有 OSS,但 api-server 仍把带背景原图字节保留到 BgFilter / 阿里云 / 本地 fallback 结束,造成并发任务下的内存峰值叠加。
|
||||
- 决策:目标链路的带背景原图上传 OSS 时消费 `DownloadedImage` 所有权,不为上传克隆整张字节缓冲;上传完成后不再跨 BgFilter 调用常驻。BgFilter 只读取 600 秒签名 URL;进入阿里云 fallback 时由 `platform-matting` 新 URL 接口下载原图、上传 `AuthorizeFileUpload` 临时对象,并在开始阿里云推理前结束下载缓冲作用域;阿里云继续失败时,api-server 再从私有 OSS 独立下载原图供本地键色,产出后释放本次原图下载缓冲。
|
||||
- 背景:角色形象、图标图集、UI 素材图集和角色动作抽取帧的带背景原图虽然已先落私有 OSS,但 api-server 仍可能把原图字节保留到 BgFilter / 阿里云 / 本地 fallback 结束,造成并发任务下的内存峰值叠加。
|
||||
- 决策:目标链路的带背景原图上传 OSS 时消费 `DownloadedImage` 或动作帧字节所有权,不为上传克隆整张字节缓冲;上传完成后不再跨 BgFilter 调用常驻。手动去背景直接复用已有 OSS object key,不下载原图。BgFilter 只读取 600 秒签名 URL;进入阿里云 fallback 时由 `platform-matting` 新 URL 接口下载原图、上传 `AuthorizeFileUpload` 临时对象,并在开始阿里云推理前结束下载缓冲作用域;阿里云继续失败时,api-server 再从私有 OSS 独立下载原图供本地键色,产出后释放本次原图下载缓冲。
|
||||
- 边界:不改变接口 DTO、资源记录、画布原图展示、图集切分行为和降级顺序;“释放”指 Rust 所有权和 `Vec<u8>` 析构,RSS 不保证同步下降。
|
||||
- 验证方式:`platform-matting` 测试覆盖 URL 下载缓冲在临时上传后结束、降尺寸 Alpha 回贴;`api-server` 结构测试覆盖带背景原图 owned 上传、URL 阿里云 fallback、本地重新下载与原图释放;随后运行两个 crate 的测试与编译检查。
|
||||
|
||||
## 2026-07-15 BgFilter 输入改用私有 OSS 短期签名 URL
|
||||
|
||||
- 背景:角色形象、图标图集和 UI 素材图集在调用 BgFilter 前已经把带背景原图持久化到私有 OSS;继续由 api-server 把同一图片作为 multipart `file` 再上传一次,会重复传输图片字节并占用 API 进程网络带宽。
|
||||
- 决策:上述生成后抠图链路统一复用已持久化原图的 object key,签发 600 秒 OSS GET URL,并通过 BgFilter multipart 的 `image_url` 字段提交;请求中不再携带 `file`。签名 URL 只交给 BgFilter,不写日志或持久化。2026-07-17 起,原图上传后不再保留图片字节;进入“阿里云通用抠图 → 本地键色”兜底链时按阶段从私有 OSS 重新下载。
|
||||
- 背景:角色形象、图标图集、UI 素材图集、角色动作抽取帧和手动去背景在调用 BgFilter 前都已有私有 OSS object key;继续由 api-server 下载或保留图片并作为 multipart `file` 再上传,会重复传输图片字节并占用 API 进程网络与内存。
|
||||
- 决策:上述抠图链路统一复用 object key,签发 600 秒 OSS GET URL,并通过 BgFilter multipart 的 `image_url` 字段提交;请求中不再携带 `file`。签名 URL 只交给 BgFilter,不写日志或持久化。2026-07-17 起,生成原图和动作帧上传后不再保留图片字节;进入“阿里云通用抠图 → 本地键色”兜底链时按阶段从私有 OSS 重新下载。
|
||||
- 影响范围:仅 `api-server/editor_project` 的 BgFilter 请求输入与相关文档;不改变 BgFilter endpoint、鉴权、`screen_color`、`seg_model`、输出校验、熔断规则、阿里云上传协议或降级顺序。
|
||||
- 验证方式:定向测试必须断言 BgFilter 请求函数包含 `image_url` 与 600 秒 OSS 换签,不包含 multipart `file` 或源图字节读取;随后运行 `cargo check -p api-server --manifest-path server-rs/Cargo.toml`。
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -68,11 +68,11 @@ HTTP 角色的 `GENARRATIVE_SPACETIME_POOL_SIZE` 只表示 procedure / reducer
|
||||
lease 过期后不代表任务一定再次执行:claim transaction 只有在 `attempt < max_attempts` 时才会递增 attempt 并返回 worker;如果过期的是最终 attempt,则直接把 job 收口为 `failed`、清理 lease,并按入队冻结价格为当前 attempt 原子退款或写 cancellation intent。该终态任务不会再次进入 provider executor,迟到 consume 会被 settlement intent 拒绝。
|
||||
|
||||
图片画布角色图、图标素材、UI 素材提取和角色动作逐帧去背景时优先调用 BgFilter;当前全部调用都显式传 `background_mode=flat`,保持单一纯色背景抠图语义。默认 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000`,该值是所有路径的基准请求超时;角色动作逐帧请求的每一次 HTTP attempt 额外增加 `2000ms × 本次实际帧数`,默认 `32 / 40 / 48` 帧对应 `244000 / 260000 / 276000ms`,角色形象单图、图标、UI 和手动去背景继续使用基准值。该 request timeout 不是整批帧或整项任务超时,首次失败后的重试会重新计时;角色动作整项任务仍受默认 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS=1800` 预算约束,排查时以 `editor_bgfilter_request_start.timeout_ms` 确认实际值。连续失败达到 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 后熔断 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300` 秒。BgFilter 调用失败和熔断期均先走阿里云通用抠图,只有阿里云失败才走本地幕布色去背景兜底。阿里云这层默认 `GENARRATIVE_ALIYUN_MATTING_ENABLED=true`,但必须在 `api-server.env` 填入 `GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID` / `GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET`(或标准 SDK 命名 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET`)才会真正启用;AccessKey 缺失时启动日志会打印「阿里云抠图 AccessKey 未配置,跳过抠图客户端初始化」,抠图直接塌成 BgFilter→本地两级,`npm run check:api-server-env` 也会给出对应告警。修改这些变量后需要重启对应 `api-server` / worker 进程;排查时先从 worker 启动日志确认 lease 和 job timeout,再看带 `background_mode=flat` 的 `editor_bgfilter_request_start`、`editor_bgfilter_fallback_to_aliyun_matting`、`editor_bgfilter_circuit_open_fallback_to_aliyun_matting`,以及阿里云失败后的 `editor_aliyun_matting_fallback_to_local_screen_background_removal` 日志。
|
||||
手动 `POST /api/editor/images/background-removals` 同样调用 BgFilter,但固定传 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传背景色;首次失败后立即重试 `1` 次,两次都失败则返回最终错误。它不进入只适用于已知纯色背景的阿里云 / 本地键色兜底链,也不改变 flat 路径的熔断状态。标准纯色背景四条链路仍固定传 `background_mode=flat`。两种模式统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN`、`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 和共享 HTTP client;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只保留为 token 兼容别名。
|
||||
手动 `POST /api/editor/images/background-removals` 同样调用 BgFilter,但 worker 只解析并校验已有 OSS object key,直接签发 600 秒 URL,不下载原图;multipart 固定传 `image_url`、`background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传 `file` 或背景色。首次失败后立即重试 `1` 次,两次都失败则返回最终错误。它不进入只适用于已知纯色背景的阿里云 / 本地键色兜底链,也不改变 flat 路径的熔断状态。标准纯色背景四条链路仍固定传 `background_mode=flat`。两种模式统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN`、`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 和共享 HTTP client;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只保留为 token 兼容别名。
|
||||
|
||||
阿里云通用抠图的非上海地域输入使用 `AuthorizeFileUpload → Policy POST → SegmentCommonImage` 正式链路,上传 Bucket / Endpoint / ObjectKey 由阿里云动态返回;不得恢复 `GetOssStsToken`、固定 `viapi-customer-temp`、临时 AK/SK 或 OSS V1 PUT。该切换不新增环境变量;真实链路冒烟可运行 `cargo run -p platform-matting --example segment_smoke --manifest-path server-rs/Cargo.toml -- <图片路径>`,预期日志中的输入 host 为授权响应返回的上海 OSS host,并完成结果下载。图片字节仍经过执行任务的 api-server / worker,排障时不要把 Advance 路径误判为阿里云直接抓取任意公网 URL。
|
||||
|
||||
BgFilter 对已经落入私有 OSS 的生成原图直接使用 600 秒签名 URL:`api-server` 的 multipart 只提交 `image_url`、`screen_color` 和 `seg_model`,不再提交 `file`,也不会在 BgFilter 调用前重新下载 OSS 对象。带背景原图上传完成后应已消费并释放字节所有权;BgFilter 失败或熔断打开后,阿里云 fallback 才单独下载源对象并上传动态临时桶,临时上传完成即释放本次下载缓冲;阿里云继续失败时本地 fallback 再独立下载,并在本地处理产出后释放本次原图缓冲。排障日志只应出现 object key 与签名有效期,不得记录带 `x-oss-*` 查询参数的完整 URL。这里的释放是 Rust 缓冲析构,不以操作系统 RSS 立即下降作为判据。
|
||||
BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去背景源图直接使用 600 秒签名 URL:`api-server` 的 multipart 提交 `image_url` 及对应模式参数,不再提交 `file`,也不会在 BgFilter 调用前重新下载 OSS 对象。生成原图和动作帧上传完成后应已消费并释放字节所有权;手动路径从始至终不读取原图字节。flat 路径的 BgFilter 失败或熔断打开后,阿里云 fallback 才单独下载源对象并上传动态临时桶,临时上传完成即释放本次下载缓冲;阿里云继续失败时本地 fallback 再独立下载,并在本地处理产出后释放本次原图缓冲。排障日志只应出现 object key 与签名有效期,不得记录带 `x-oss-*` 查询参数的完整 URL。这里的释放是 Rust 缓冲析构,不以操作系统 RSS 立即下降作为判据。
|
||||
|
||||
`我的` 页签或排障面板展示队列等待时,只读取 BFF 队列接口:`GET /api/runtime/external-generation/queue-overview` 查看当前用户可见队列概览,`GET /api/runtime/external-generation/jobs/{jobId}` 查看单 job 状态。生成页 / 进度页不承接队列概览,只展示当前玩法业务进度;队列接口只提供等待 / 运行 / 失败 / 完成状态补充,最终草稿、作品和结果页仍要轮询对应玩法 session/detail 接口收敛到 ready 或 failed;不要直接查询 `external_generation_job` private table,也不要把 worker 内部 payload 暴露到前端。
|
||||
|
||||
|
||||
@@ -10,39 +10,6 @@ use crate::{
|
||||
http_error::AppError, openai_image_generation::DownloadedOpenAiImage, state::AppState,
|
||||
};
|
||||
|
||||
/// 图片字节 → 阿里云通用抠图 → 原尺寸透明 PNG。
|
||||
/// 未配置抠图客户端时返回错误,由调用方决定是否降级本地算法。
|
||||
pub(crate) async fn segment_image_with_aliyun_matting(
|
||||
state: &AppState,
|
||||
image: &DownloadedOpenAiImage,
|
||||
log_label: &str,
|
||||
) -> Result<DownloadedOpenAiImage, AppError> {
|
||||
let matting_client = state
|
||||
.matting_client()
|
||||
.ok_or_else(aliyun_matting_unconfigured_error)?;
|
||||
|
||||
let file_name = format!("{log_label}.png");
|
||||
let started_at = std::time::Instant::now();
|
||||
let output_bytes = matting_client
|
||||
.segment_image_to_transparent_png(image.bytes.as_slice(), &file_name)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
aliyun_matting_failure_to_app_error(&error, started_at.elapsed().as_millis() as u64)
|
||||
})?;
|
||||
tracing::info!(
|
||||
provider = "aliyun-matting",
|
||||
log_label,
|
||||
elapsed_ms = started_at.elapsed().as_millis() as u64,
|
||||
"阿里云通用抠图完成"
|
||||
);
|
||||
|
||||
Ok(DownloadedOpenAiImage {
|
||||
bytes: output_bytes,
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 私有 OSS 签名 URL → 延迟下载 → 阿里云临时桶 → 原尺寸透明 PNG。
|
||||
/// 下载和临时上传由 platform-matting 收口,源图缓冲不会跨越整次阿里云推理常驻。
|
||||
pub(crate) async fn segment_image_url_with_aliyun_matting(
|
||||
|
||||
@@ -77,7 +77,6 @@ use crate::{
|
||||
resolve_editor_screen_background_color,
|
||||
},
|
||||
http_error::AppError,
|
||||
openai_image_generation::DownloadedOpenAiImage,
|
||||
platform_errors::map_oss_error,
|
||||
prompt::role_asset_studio::{
|
||||
build_role_asset_workflow, normalize_animation_prompt_text_by_key,
|
||||
@@ -2391,7 +2390,7 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
) -> Result<ProcessedEditorCharacterAnimationFrame, AppError> {
|
||||
// 中文注释:每一帧只要求自己的绿幕源图先落 OSS,不再等待整批源图全部上传完成。
|
||||
put_character_animation_object(
|
||||
let source_put = put_character_animation_object(
|
||||
state,
|
||||
LegacyAssetPrefix::Animations,
|
||||
vec![
|
||||
@@ -2405,7 +2404,7 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
frame.extension
|
||||
),
|
||||
frame.mime_type.clone(),
|
||||
frame.bytes.clone(),
|
||||
frame.bytes,
|
||||
build_asset_metadata(
|
||||
EDITOR_CHARACTER_ANIMATION_ASSET_KIND,
|
||||
owner_user_id,
|
||||
@@ -2417,14 +2416,9 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let image = DownloadedOpenAiImage {
|
||||
bytes: frame.bytes,
|
||||
mime_type: frame.mime_type,
|
||||
extension: frame.extension,
|
||||
};
|
||||
let removed = remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
|
||||
state,
|
||||
&image,
|
||||
source_put.object_key.as_str(),
|
||||
screen_color,
|
||||
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
||||
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
|
||||
@@ -6147,6 +6141,7 @@ mod tests {
|
||||
"put_character_animation_object",
|
||||
"green-screen-frame",
|
||||
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"source_put.object_key.as_str()",
|
||||
"bgfilter_request_timeout_ms",
|
||||
"finalize_animation_frame_payload",
|
||||
"put_character_animation_object",
|
||||
@@ -6168,6 +6163,13 @@ mod tests {
|
||||
"finalize_animation_frame_payload",
|
||||
],
|
||||
);
|
||||
let frame_pipeline = source
|
||||
.split_once("async fn process_and_persist_editor_character_animation_frame")
|
||||
.and_then(|(_, tail)| tail.split_once("async fn publish_animation_set"))
|
||||
.map(|(body, _)| body)
|
||||
.expect("frame pipeline function should exist");
|
||||
assert!(!frame_pipeline.contains("frame.bytes.clone()"));
|
||||
assert!(!frame_pipeline.contains("DownloadedOpenAiImage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user