拆分大文件

This commit is contained in:
2026-04-23 23:38:00 +08:00
parent 53a9cdd791
commit 8df502b2a7
506 changed files with 11312 additions and 13069 deletions

View File

@@ -735,7 +735,7 @@ async fn persist_animation_preview_video(
"provider": "character-animation",
"message": "当前策略需要真实生成视频结果,不再支持回退到仓库占位预览视频。",
})),
)
);
}
};
let put_result = put_character_animation_object(
@@ -1005,7 +1005,9 @@ async fn send_ark_image_to_video_request(
}))
.send()
.await
.map_err(|error| map_character_animation_upstream_error(format!("请求 Ark 视频服务失败:{error}")))
.map_err(|error| {
map_character_animation_upstream_error(format!("请求 Ark 视频服务失败:{error}"))
})
}
async fn wait_for_ark_content_generation_task(
@@ -1026,7 +1028,9 @@ async fn wait_for_ark_content_generation_task(
)
.send()
.await
.map_err(|error| map_character_animation_upstream_error(format!("查询 Ark 视频任务失败:{error}")))?;
.map_err(|error| {
map_character_animation_upstream_error(format!("查询 Ark 视频任务失败:{error}"))
})?;
let status = response.status();
let text = response.text().await.map_err(|error| {
map_character_animation_upstream_error(format!("读取 Ark 视频任务响应失败:{error}"))
@@ -1062,11 +1066,13 @@ async fn wait_for_ark_content_generation_task(
sleep(Duration::from_millis(ARK_VIDEO_TASK_POLL_INTERVAL_MS)).await;
}
Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "ark",
"message": "视频生成任务执行超时,请稍后重试。",
"taskId": task_id,
})))
Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "ark",
"message": "视频生成任务执行超时,请稍后重试。",
"taskId": task_id,
})),
)
}
async fn download_generated_video(
@@ -1074,11 +1080,9 @@ async fn download_generated_video(
video_url: &str,
fallback_message: &str,
) -> Result<MediaPayload, AppError> {
let response = http_client
.get(video_url)
.send()
.await
.map_err(|error| map_character_animation_upstream_error(format!("{fallback_message}{error}")))?;
let response = http_client.get(video_url).send().await.map_err(|error| {
map_character_animation_upstream_error(format!("{fallback_message}{error}"))
})?;
let status = response.status();
let content_type = response
.headers()
@@ -1090,11 +1094,13 @@ async fn download_generated_video(
map_character_animation_upstream_error(format!("{fallback_message}{error}"))
})?;
if !status.is_success() {
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "character-animation",
"message": fallback_message,
"status": status.as_u16(),
})));
return Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "character-animation",
"message": fallback_message,
"status": status.as_u16(),
})),
);
}
Ok(MediaPayload {
mime_type: content_type.clone(),
@@ -1728,12 +1734,9 @@ fn build_character_animation_prompt(
loop_,
use_chroma_key,
),
CharacterAnimationStrategy::ImageSequence => build_image_sequence_prompt(
animation,
prompt_text,
frame_count,
use_chroma_key,
),
CharacterAnimationStrategy::ImageSequence => {
build_image_sequence_prompt(animation, prompt_text, frame_count, use_chroma_key)
}
CharacterAnimationStrategy::MotionTransfer
| CharacterAnimationStrategy::ReferenceToVideo => build_npc_animation_prompt(
animation,
@@ -1755,7 +1758,10 @@ fn build_image_sequence_prompt(
use_chroma_key: bool,
) -> String {
[
format!("同一角色连续 {} 帧动作序列,动作主题是 {}", frame_count, animation),
format!(
"同一角色连续 {} 帧动作序列,动作主题是 {}",
frame_count, animation
),
"固定机位,单人,全身,侧身朝右,保持同一套服装、发型、武器和体型。".to_string(),
"帧间动作连续,姿态逐步推进,不要换人,不要跳变,不要多余物体。".to_string(),
if use_chroma_key {
@@ -1784,16 +1790,16 @@ fn build_npc_animation_prompt(
let character_brief = build_compact_animation_character_brief(character_brief_text);
let action_detail_text = sanitize_animation_prompt_text(prompt_text, 140);
let loop_rule = if loop_ {
"这是循环动作,直接进入动作循环中段,不要开场静止站桩,不要把主参考图原样作为第一帧。".to_string()
"这是循环动作,直接进入动作循环中段,不要开场静止站桩,不要把主参考图原样作为第一帧。"
.to_string()
} else if animation == "die" {
"这是死亡终结动作,首帧参考主图角色形象即可,尾帧停在死亡结束姿态,不要回到主图形象。".to_string()
"这是死亡终结动作,首帧参考主图角色形象即可,尾帧停在死亡结束姿态,不要回到主图形象。"
.to_string()
} else {
"这是非循环动作,首帧和尾帧都要回到参考主图角色形象,中段完成动作变化。".to_string()
};
if let Some(template) = action_template_id
.and_then(|id| find_motion_template(id))
{
if let Some(template) = action_template_id.and_then(|id| find_motion_template(id)) {
return [
format!(
"单人 NPC 全身动作视频,动作主题是 {}。角色固定为同一人,右向斜侧身,镜头稳定,轮廓清晰,武器不可丢失。",
@@ -1843,7 +1849,11 @@ fn build_npc_animation_prompt(
} else {
action_detail_text
},
format!("目标帧率 {} fps时长约 {} 秒。", fps.clamp(1, 60), duration_seconds.clamp(1, 8)),
format!(
"目标帧率 {} fps时长约 {} 秒。",
fps.clamp(1, 60),
duration_seconds.clamp(1, 8)
),
loop_rule,
]
.into_iter()
@@ -1906,8 +1916,12 @@ fn build_ark_character_animation_prompt(
}
[
format!("单人 NPC 全身动作视频,动作英文名是 {}", normalized_animation_name),
"角色固定为图片1和图片2中的同一人侧身朝右镜头稳定轮廓清晰武器不可丢失。".to_string(),
format!(
"单人 NPC 全身动作视频,动作英文名是 {}",
normalized_animation_name
),
"角色固定为图片1和图片2中的同一人侧身朝右镜头稳定轮廓清晰武器不可丢失。"
.to_string(),
"动作连贯,避免服装、发型、面部、武器随机漂移,不要多角色,不要镜头切换。".to_string(),
if use_chroma_key {
"背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。".to_string()
@@ -2341,11 +2355,7 @@ fn finalize_animation_frame_payload(
);
}
let normalized = contain_rgba_image(
&image,
frame_width.max(1),
frame_height.max(1),
);
let normalized = contain_rgba_image(&image, frame_width.max(1), frame_height.max(1));
let mut encoded = Vec::new();
let encoder = PngEncoder::new(&mut encoded);
encoder
@@ -2665,7 +2675,14 @@ fn is_completed_generation_task_status(status: &str) -> bool {
fn is_failed_generation_task_status(status: &str) -> bool {
matches!(
status,
"failed" | "canceled" | "cancelled" | "error" | "aborted" | "rejected" | "expired" | "unknown"
"failed"
| "canceled"
| "cancelled"
| "error"
| "aborted"
| "rejected"
| "expired"
| "unknown"
)
}
@@ -3110,9 +3127,8 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
}
let offset = pixel_index * 4;
let alpha = pixels[offset + 3];
let strong_candidate = alpha < 40
|| green_scores[pixel_index] > 0.12
|| white_scores[pixel_index] > 0.32;
let strong_candidate =
alpha < 40 || green_scores[pixel_index] > 0.12 || white_scores[pixel_index] > 0.32;
if !strong_candidate {
return;
}
@@ -3137,9 +3153,21 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
let y = pixel_index / width;
let neighbor_indexes = [
if x > 0 { Some(pixel_index - 1) } else { None },
if x + 1 < width { Some(pixel_index + 1) } else { None },
if y > 0 { Some(pixel_index - width) } else { None },
if y + 1 < height { Some(pixel_index + width) } else { None },
if x + 1 < width {
Some(pixel_index + 1)
} else {
None
},
if y > 0 {
Some(pixel_index - width)
} else {
None
},
if y + 1 < height {
Some(pixel_index + width)
} else {
None
},
];
for next_pixel_index in neighbor_indexes.into_iter().flatten() {
@@ -3153,9 +3181,7 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
let next_hint = background_hints[next_pixel_index];
let reachable_soft_edge = next_hint > 0.08
&& next_alpha < SOFT_EDGE_ALPHA_THRESHOLD
&& (next_green_score > 0.04
|| next_white_score > 0.08
|| next_alpha < 180);
&& (next_green_score > 0.04 || next_white_score > 0.08 || next_alpha < 180);
if next_alpha < 40
|| next_green_score > 0.12
@@ -3203,8 +3229,7 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
}
}
if adjacent_background_count >= 2
|| (adjacent_background_count >= 1 && hint > 0.18)
if adjacent_background_count >= 2 || (adjacent_background_count >= 1 && hint > 0.18)
{
expanded_mask[pixel_index] = 1;
}
@@ -3237,10 +3262,7 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
}
let next_x = x as i32 + offset_x;
let next_y = y as i32 + offset_y;
if next_x < 0
|| next_x >= width as i32
|| next_y < 0
|| next_y >= height as i32
if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32
{
continue;
}
@@ -3295,10 +3317,7 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
}
let next_x = x as i32 + offset_x;
let next_y = y as i32 + offset_y;
if next_x < 0
|| next_x >= width as i32
|| next_y < 0
|| next_y >= height as i32
if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32
{
touches_transparent_edge = true;
continue;
@@ -3320,7 +3339,11 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
let white_score = white_scores[pixel_index];
let contamination = green_score
.max(white_score)
.max(if background_mask[pixel_index] != 0 { 0.35 } else { 0.0 })
.max(if background_mask[pixel_index] != 0 {
0.35
} else {
0.0
})
.max(if alpha < 220 {
((220 - alpha) as f32 / 220.0) * 0.25
} else {
@@ -3343,7 +3366,8 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
&background_mask,
&background_hints,
);
let blend = clamp01(contamination.max(if touches_transparent_edge { 0.22 } else { 0.0 }));
let blend =
clamp01(contamination.max(if touches_transparent_edge { 0.22 } else { 0.0 }));
if let Some((sample_red, sample_green, sample_blue)) = sample {
red = lerp(red, sample_red as f32, blend);
@@ -3360,7 +3384,8 @@ fn remove_background_from_rgba(pixels: &mut [u8], width: usize, height: usize) -
}
} else {
if green_score > 0.04 {
green = green.max(red.max(blue))
green = green
.max(red.max(blue))
.max((green - (green - red.max(blue)) * 0.78).round());
}