按帧数扩展角色动作抠图超时

按基准超时加每帧两秒计算角色动作批量帧的单请求超时
在共享 BgFilter 客户端上覆盖请求级超时并保持普通路径、重试和降级行为
补充动态超时、请求覆盖和全帧流水线测试
同步后端、运维、画布专题及项目决策文档
This commit is contained in:
2026-07-15 11:48:23 +00:00
parent b148895e29
commit fddf30d7c3
7 changed files with 161 additions and 17 deletions
@@ -70,7 +70,7 @@ use crate::{
apply_editor_screen_background_decision_to_generation_inputs,
build_editor_canvas_generated_layer_item, complete_editor_canvas_generation_with_items,
persist_editor_generated_media_asset,
remove_editor_generated_screen_background_with_bgfilter,
remove_editor_generated_screen_background_with_bgfilter_with_request_timeout,
},
editor_screen_background_decision::{
EditorScreenBackgroundDecisionInput, EditorScreenBackgroundDecisionKind,
@@ -116,6 +116,7 @@ const EDITOR_CHARACTER_ANIMATION_MODEL: &str = "seedance2.0-fast";
const EDITOR_CHARACTER_ANIMATION_ASSET_KIND: &str = "editor_character_animation";
const EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND: &str = "character-animation";
const EDITOR_CHARACTER_ANIMATION_PROVIDER_SOURCE_SLOT: &str = "provider_source";
const EDITOR_CHARACTER_ANIMATION_BGFILTER_TIMEOUT_PER_FRAME_MS: u64 = 2_000;
const EDITOR_VIDEO_ASSET_KIND: &str = "editor_video";
const EDITOR_VIDEO_ENTITY_KIND: &str = "editor_canvas";
const EDITOR_VIDEO_SLOT: &str = "video_preview";
@@ -2261,6 +2262,18 @@ async fn create_editor_ark_image_to_video_task(
})
}
fn editor_character_animation_bgfilter_request_timeout_ms(
base_timeout_ms: u64,
frame_count: usize,
) -> u64 {
let frame_timeout_increment_ms = u64::try_from(frame_count)
.unwrap_or(u64::MAX)
.saturating_mul(EDITOR_CHARACTER_ANIMATION_BGFILTER_TIMEOUT_PER_FRAME_MS);
base_timeout_ms
.saturating_add(frame_timeout_increment_ms)
.max(1)
}
async fn extract_and_persist_editor_character_animation_frames(
state: &AppState,
owner_user_id: &str,
@@ -2290,6 +2303,10 @@ async fn extract_and_persist_editor_character_animation_frames(
use futures_util::StreamExt as _;
let frame_count = finalized_frames.len();
let bgfilter_request_timeout_ms = editor_character_animation_bgfilter_request_timeout_ms(
state.config.editor_bgfilter_request_timeout_ms,
frame_count,
);
let frame_results = futures_util::stream::iter(finalized_frames.into_iter().enumerate().map(
|(frame_index, frame)| async move {
process_and_persist_editor_character_animation_frame(
@@ -2302,6 +2319,7 @@ async fn extract_and_persist_editor_character_animation_frames(
request.frame_width,
request.frame_height,
request.screen_color,
bgfilter_request_timeout_ms,
audit,
)
.await
@@ -2365,6 +2383,7 @@ async fn process_and_persist_editor_character_animation_frame(
frame_width: u32,
frame_height: u32,
screen_color: EditorScreenBackgroundColor,
bgfilter_request_timeout_ms: u64,
audit: &crate::external_api_audit::ExternalApiAuditContext,
) -> Result<ProcessedEditorCharacterAnimationFrame, AppError> {
// 中文注释:每一帧只要求自己的绿幕源图先落 OSS,不再等待整批源图全部上传完成。
@@ -2399,12 +2418,13 @@ async fn process_and_persist_editor_character_animation_frame(
mime_type: frame.mime_type,
extension: frame.extension,
};
let removed = remove_editor_generated_screen_background_with_bgfilter(
let removed = remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
state,
&image,
screen_color,
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
bgfilter_request_timeout_ms,
audit,
)
.await?;
@@ -6063,6 +6083,34 @@ mod tests {
);
}
#[test]
fn editor_character_animation_bgfilter_timeout_scales_with_frame_count() {
assert_eq!(
editor_character_animation_bgfilter_request_timeout_ms(180_000, 32),
244_000
);
assert_eq!(
editor_character_animation_bgfilter_request_timeout_ms(180_000, 40),
260_000
);
assert_eq!(
editor_character_animation_bgfilter_request_timeout_ms(180_000, 48),
276_000
);
assert_eq!(
editor_character_animation_bgfilter_request_timeout_ms(0, 32),
64_000
);
assert_eq!(
editor_character_animation_bgfilter_request_timeout_ms(0, 0),
1
);
assert_eq!(
editor_character_animation_bgfilter_request_timeout_ms(u64::MAX - 1, 48),
u64::MAX
);
}
#[test]
fn editor_character_animation_frames_use_three_stage_matting_fallback() {
let source = include_str!("character_animation_assets.rs");
@@ -6072,7 +6120,10 @@ mod tests {
"async fn publish_animation_set",
&[
"apply_chroma_key: false",
"editor_character_animation_bgfilter_request_timeout_ms",
"state.config.editor_bgfilter_request_timeout_ms",
"process_and_persist_editor_character_animation_frame",
"bgfilter_request_timeout_ms",
".buffer_unordered(frame_count.max(1))",
".collect::<Vec<_>>()",
"frame_payloads.sort_by_key",
@@ -6086,7 +6137,8 @@ mod tests {
&[
"put_character_animation_object",
"green-screen-frame",
"remove_editor_generated_screen_background_with_bgfilter",
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
"bgfilter_request_timeout_ms",
"finalize_animation_frame_payload",
"put_character_animation_object",
"animation_frame",
@@ -6100,9 +6152,10 @@ mod tests {
"EDITOR_CHARACTER_ANIMATION_ASSET_KIND",
"EDITOR_CHARACTER_ANIMATION_PROVIDER_SOURCE_SLOT",
"green-screen-frame",
"remove_editor_generated_screen_background_with_bgfilter",
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
"EDITOR_BGFILTER_DEFAULT_SEG_MODEL",
"EDITOR_BGFILTER_CROSS_CHECK_ENABLED",
"bgfilter_request_timeout_ms",
"finalize_animation_frame_payload",
],
);
@@ -2845,6 +2845,27 @@ pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(
seg_model: &str,
cross_check: bool,
audit: &crate::external_api_audit::ExternalApiAuditContext,
) -> Result<EditorScreenBackgroundRemovalOutput, AppError> {
remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
state,
image,
screen_color,
seg_model,
cross_check,
state.config.editor_bgfilter_request_timeout_ms,
audit,
)
.await
}
pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
state: &AppState,
image: &DownloadedOpenAiImage,
screen_color: EditorScreenBackgroundColor,
seg_model: &str,
cross_check: bool,
request_timeout_ms: u64,
audit: &crate::external_api_audit::ExternalApiAuditContext,
) -> Result<EditorScreenBackgroundRemovalOutput, AppError> {
if let Some(remaining) = editor_bgfilter_circuit_open_remaining(state) {
// 熔断打开只跳过 BgFilter 调用本身,仍要走阿里云兜底;绝不能在 cooldown 内直接退化到
@@ -2869,6 +2890,7 @@ pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(
screen_color,
seg_model,
cross_check,
request_timeout_ms,
)
.await
{
@@ -3045,11 +3067,12 @@ async fn request_editor_generated_screen_background_with_bgfilter(
screen_color: EditorScreenBackgroundColor,
seg_model: &str,
cross_check: bool,
request_timeout_ms: u64,
) -> Result<DownloadedOpenAiImage, AppError> {
let url = editor_bgfilter_endpoint(state)?;
let call_id = format!("bgfilter-call-{}", current_utc_micros());
let request_started_at = Instant::now();
let timeout_ms = state.config.editor_bgfilter_request_timeout_ms.max(1);
let timeout_ms = request_timeout_ms.max(1);
let input_bytes = image.bytes.len();
let source_mime_type = image.mime_type.clone();
let source_file_name = format!(
@@ -3088,7 +3111,10 @@ async fn request_editor_generated_screen_background_with_bgfilter(
"cross_check",
editor_bgfilter_cross_check_form_value(cross_check),
);
let mut request = http_client.post(url.as_str()).multipart(form);
let mut request = http_client
.post(url.as_str())
.timeout(Duration::from_millis(timeout_ms))
.multipart(form);
if let Some(token) = state
.config
.editor_bgfilter_token
@@ -7098,6 +7124,13 @@ mod tests {
fn spawn_bgfilter_png_mock(
response_png: Vec<u8>,
) -> (String, mpsc::Receiver<Vec<u8>>, thread::JoinHandle<()>) {
spawn_bgfilter_png_mock_with_delay(response_png, Duration::ZERO)
}
fn spawn_bgfilter_png_mock_with_delay(
response_png: Vec<u8>,
response_delay: Duration,
) -> (String, mpsc::Receiver<Vec<u8>>, thread::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("mock listener should bind");
let address = listener
@@ -7110,6 +7143,7 @@ mod tests {
request_sender
.send(request)
.expect("captured request should be delivered");
thread::sleep(response_delay);
let headers = format!(
"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nX-BgFilter-Elapsed-Ms: 9\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
response_png.len()
@@ -9449,6 +9483,44 @@ mod tests {
assert_eq!(EDITOR_BGFILTER_BACKGROUND_MODE_COMPLEX, "complex");
}
#[tokio::test]
async fn editor_generated_bgfilter_request_timeout_overrides_shared_client_default() {
let response_png = encode_test_png(3, 2);
let (base_url, request_receiver, server) =
spawn_bgfilter_png_mock_with_delay(response_png.clone(), Duration::from_millis(100));
let state = AppState::new(AppConfig {
editor_bgfilter_base_url: base_url,
editor_bgfilter_request_timeout_ms: 20,
..AppConfig::default()
})
.expect("state should build");
let source_image = DownloadedOpenAiImage {
bytes: encode_test_png(2, 2),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
let removed = request_editor_generated_screen_background_with_bgfilter(
&state,
&source_image,
parse_editor_screen_background_color(Some("#CFEFFF"))
.expect("screen color should parse"),
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
2_000,
)
.await
.expect("request timeout override should outlive the shared client default");
request_receiver
.recv_timeout(Duration::from_secs(1))
.expect("mock server should capture request");
server.join().expect("mock server should stop cleanly");
assert_eq!(removed.bytes, response_png);
assert_eq!(removed.mime_type, "image/png");
assert_eq!(removed.extension, "png");
}
#[tokio::test]
async fn editor_manual_background_removal_sends_complex_bgfilter_http_contract() {
let response_png = encode_test_png(3, 2);
@@ -9650,9 +9722,18 @@ mod tests {
"EDITOR_BGFILTER_CROSS_CHECK_DISABLED",
],
);
assert_function_contains(
source,
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(",
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
&[
"state.config.editor_bgfilter_request_timeout_ms",
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
],
);
assert_function_contains_in_order(
source,
"async fn remove_editor_generated_screen_background_with_bgfilter",
"async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
"async fn fallback_editor_screen_background_removal",
&[
// 熔断打开分支必须先委派统一兜底链(阿里云→本地),不能直接退化本地扣色;
@@ -9678,11 +9759,12 @@ mod tests {
assert_function_contains(
source,
"async fn request_editor_generated_screen_background_with_bgfilter",
"async fn request_editor_background_removal_image",
"async fn request_editor_background_removal_image(",
&[
"editor_bgfilter_endpoint",
"editor_bgfilter_request_timeout_ms.max(1)",
"request_timeout_ms.max(1)",
"state.editor_bgfilter_http_client()",
".timeout(Duration::from_millis(timeout_ms))",
"\"screen_color\"",
"\"seg_model\"",
"seg_model.to_string()",
@@ -9695,12 +9777,12 @@ mod tests {
assert_function_not_contains(
source,
"async fn request_editor_generated_screen_background_with_bgfilter",
"async fn request_editor_background_removal_image",
"async fn request_editor_background_removal_image(",
&["reqwest::Client::builder"],
);
assert_function_contains(
source,
"async fn request_editor_background_removal_image",
"async fn request_editor_background_removal_image(",
"fn editor_bgfilter_endpoint",
&[
"editor_bgfilter_endpoint",
@@ -9718,7 +9800,7 @@ mod tests {
);
assert_function_not_contains(
source,
"async fn request_editor_background_removal_image",
"async fn request_editor_background_removal_image(",
"fn editor_bgfilter_endpoint",
&["\"screen_color\"", "reqwest::Client::builder"],
);
@@ -9790,7 +9872,7 @@ mod tests {
assert!(source.contains("const EDITOR_BGFILTER_RETRY_COUNT: usize = 1;"));
assert_function_contains_in_order(
source,
"async fn remove_editor_generated_screen_background_with_bgfilter",
"async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
"async fn fallback_editor_screen_background_removal",
&[
"let max_attempts = EDITOR_BGFILTER_RETRY_COUNT + 1",