按帧数扩展角色动作抠图超时
按基准超时加每帧两秒计算角色动作批量帧的单请求超时 在共享 BgFilter 客户端上覆盖请求级超时并保持普通路径、重试和降级行为 补充动态超时、请求覆盖和全帧流水线测试 同步后端、运维、画布专题及项目决策文档
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user