Resolve spacetime client binding merge conflicts
This commit is contained in:
@@ -43,7 +43,12 @@ use shared_contracts::assets::{
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
|
||||
api_response::json_success_body,
|
||||
custom_world_asset_prompts::{
|
||||
build_character_animation_prompt, build_fallback_moderation_safe_animation_prompt,
|
||||
},
|
||||
http_error::AppError,
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
};
|
||||
use tokio::time::sleep;
|
||||
@@ -1713,310 +1718,11 @@ fn build_character_animation_job_payload(task: AiTaskSnapshot) -> CharacterAsset
|
||||
}
|
||||
}
|
||||
|
||||
fn build_character_animation_prompt(
|
||||
strategy: &CharacterAnimationStrategy,
|
||||
prompt_text: &str,
|
||||
character_brief_text: Option<&str>,
|
||||
action_template_id: Option<&str>,
|
||||
animation: &str,
|
||||
frame_count: u32,
|
||||
fps: u32,
|
||||
duration_seconds: u32,
|
||||
loop_: bool,
|
||||
use_chroma_key: bool,
|
||||
) -> String {
|
||||
match strategy {
|
||||
CharacterAnimationStrategy::ImageToVideo => build_ark_character_animation_prompt(
|
||||
animation,
|
||||
prompt_text,
|
||||
character_brief_text,
|
||||
action_template_id,
|
||||
loop_,
|
||||
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,
|
||||
prompt_text,
|
||||
character_brief_text,
|
||||
action_template_id,
|
||||
loop_,
|
||||
use_chroma_key,
|
||||
fps,
|
||||
duration_seconds,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_image_sequence_prompt(
|
||||
animation: &str,
|
||||
prompt_text: &str,
|
||||
frame_count: u32,
|
||||
use_chroma_key: bool,
|
||||
) -> String {
|
||||
[
|
||||
format!(
|
||||
"同一角色连续 {} 帧动作序列,动作主题是 {}。",
|
||||
frame_count, animation
|
||||
),
|
||||
"固定机位,单人,全身,侧身朝右,保持同一套服装、发型、武器和体型。".to_string(),
|
||||
"帧间动作连续,姿态逐步推进,不要换人,不要跳变,不要多余物体。".to_string(),
|
||||
if use_chroma_key {
|
||||
"纯绿色背景,无地面装饰,方便后期抠像。".to_string()
|
||||
} else {
|
||||
"背景尽量纯净,避免复杂场景。".to_string()
|
||||
},
|
||||
prompt_text.trim().to_string(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn build_npc_animation_prompt(
|
||||
animation: &str,
|
||||
prompt_text: &str,
|
||||
character_brief_text: Option<&str>,
|
||||
action_template_id: Option<&str>,
|
||||
loop_: bool,
|
||||
use_chroma_key: bool,
|
||||
fps: u32,
|
||||
duration_seconds: u32,
|
||||
) -> String {
|
||||
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()
|
||||
} else if animation == "die" {
|
||||
"这是死亡终结动作,首帧参考主图角色形象即可,尾帧停在死亡结束姿态,不要回到主图形象。"
|
||||
.to_string()
|
||||
} else {
|
||||
"这是非循环动作,首帧和尾帧都要回到参考主图角色形象,中段完成动作变化。".to_string()
|
||||
};
|
||||
|
||||
if let Some(template) = action_template_id.and_then(|id| find_motion_template(id)) {
|
||||
return [
|
||||
format!(
|
||||
"单人 NPC 全身动作视频,动作主题是 {}。角色固定为同一人,右向斜侧身,镜头稳定,轮廓清晰,武器不可丢失。",
|
||||
template.animation
|
||||
),
|
||||
if use_chroma_key {
|
||||
"背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。".to_string()
|
||||
} else {
|
||||
"背景简洁纯净,无复杂场景。".to_string()
|
||||
},
|
||||
if character_brief.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("角色设定:{}。", character_brief)
|
||||
},
|
||||
format!("动作补充:{}。", template.prompt_suffix),
|
||||
if action_detail_text.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("动作细节:{}。", action_detail_text)
|
||||
},
|
||||
format!("目标帧率 {} fps,时长约 {} 秒。", fps.clamp(1, 60), duration_seconds.clamp(1, 8)),
|
||||
loop_rule,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
[
|
||||
format!("单人 NPC 全身动作视频,动作主题是 {}。", animation),
|
||||
"角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。".to_string(),
|
||||
"动作连贯,避免服装、发型、面部、武器随机漂移。".to_string(),
|
||||
if use_chroma_key {
|
||||
"背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。".to_string()
|
||||
} else {
|
||||
"背景简洁纯净,无复杂场景。".to_string()
|
||||
},
|
||||
if character_brief.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("角色设定:{}。", character_brief)
|
||||
},
|
||||
if action_detail_text.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
action_detail_text
|
||||
},
|
||||
format!(
|
||||
"目标帧率 {} fps,时长约 {} 秒。",
|
||||
fps.clamp(1, 60),
|
||||
duration_seconds.clamp(1, 8)
|
||||
),
|
||||
loop_rule,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn build_ark_character_animation_prompt(
|
||||
animation: &str,
|
||||
prompt_text: &str,
|
||||
character_brief_text: Option<&str>,
|
||||
action_template_id: Option<&str>,
|
||||
loop_: bool,
|
||||
use_chroma_key: bool,
|
||||
) -> String {
|
||||
let normalized_animation_name = animation.trim().replace(char::is_whitespace, "_");
|
||||
let normalized_animation_name = if normalized_animation_name.is_empty() {
|
||||
"idle".to_string()
|
||||
} else {
|
||||
normalized_animation_name
|
||||
};
|
||||
let character_brief = build_compact_animation_character_brief(character_brief_text);
|
||||
let action_detail_text = sanitize_animation_prompt_text(prompt_text, 140);
|
||||
let frame_rule = if loop_ {
|
||||
"首帧严格使用图片1,尾帧严格使用图片2,循环动作必须自然闭环,不要静止开场。".to_string()
|
||||
} else {
|
||||
"首帧严格使用图片1,尾帧严格使用图片2,中段完成完整动作变化,收束干净。".to_string()
|
||||
};
|
||||
|
||||
if let Some(template) = action_template_id.and_then(find_motion_template) {
|
||||
return [
|
||||
format!(
|
||||
"单人 NPC 全身动作视频,动作英文名是 {}。角色固定为图片1和图片2中的同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。",
|
||||
normalized_animation_name
|
||||
),
|
||||
"动作连贯,避免服装、发型、面部、武器随机漂移,不要多角色,不要镜头切换。".to_string(),
|
||||
if use_chroma_key {
|
||||
"背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。".to_string()
|
||||
} else {
|
||||
"背景简洁纯净,无复杂场景。".to_string()
|
||||
},
|
||||
if character_brief.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("角色设定:{}。", character_brief)
|
||||
},
|
||||
format!("动作补充:{}。", template.prompt_suffix),
|
||||
if action_detail_text.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("动作细节:{}。", action_detail_text)
|
||||
},
|
||||
frame_rule,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
[
|
||||
format!(
|
||||
"单人 NPC 全身动作视频,动作英文名是 {}。",
|
||||
normalized_animation_name
|
||||
),
|
||||
"角色固定为图片1和图片2中的同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。"
|
||||
.to_string(),
|
||||
"动作连贯,避免服装、发型、面部、武器随机漂移,不要多角色,不要镜头切换。".to_string(),
|
||||
if use_chroma_key {
|
||||
"背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。".to_string()
|
||||
} else {
|
||||
"背景简洁纯净,无复杂场景。".to_string()
|
||||
},
|
||||
if character_brief.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("角色设定:{}。", character_brief)
|
||||
},
|
||||
if action_detail_text.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("动作细节:{}。", action_detail_text)
|
||||
},
|
||||
frame_rule,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn build_fallback_moderation_safe_animation_prompt(
|
||||
animation: &str,
|
||||
loop_: bool,
|
||||
use_chroma_key: bool,
|
||||
) -> String {
|
||||
[
|
||||
format!("单人全身角色动作视频,动作主题是 {}。", animation),
|
||||
"角色固定为同一人,右向斜侧身,镜头稳定,轮廓清楚。".to_string(),
|
||||
if loop_ {
|
||||
"循环动作直接进入稳定循环,不要静止开场,不要定格首帧。".to_string()
|
||||
} else {
|
||||
"非循环动作首尾回到角色标准站姿,中段完成动作变化。".to_string()
|
||||
},
|
||||
if use_chroma_key {
|
||||
"背景为纯绿色绿幕,无其他人物和场景元素。".to_string()
|
||||
} else {
|
||||
"背景简洁纯净。".to_string()
|
||||
},
|
||||
]
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn sanitize_animation_prompt_text(value: &str, max_length: usize) -> String {
|
||||
value
|
||||
.replace(char::is_whitespace, " ")
|
||||
.replace("血浆", "")
|
||||
.replace("喷血", "")
|
||||
.replace("鲜血", "")
|
||||
.replace("断肢", "")
|
||||
.replace("斩首", "")
|
||||
.replace("裸体", "")
|
||||
.replace("裸露", "")
|
||||
.replace("色情", "")
|
||||
.replace("性交", "")
|
||||
.replace("死亡", "倒地结束")
|
||||
.replace("死去", "倒地结束")
|
||||
.replace("击杀", "倒地结束")
|
||||
.replace("受击", "失衡")
|
||||
.replace("受伤", "失衡")
|
||||
.replace("砍杀", "挥击")
|
||||
.replace("斩击", "挥击")
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.chars()
|
||||
.take(max_length)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn build_compact_animation_character_brief(value: Option<&str>) -> String {
|
||||
let normalized = sanitize_animation_prompt_text(value.unwrap_or_default(), 160);
|
||||
if normalized.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
normalized
|
||||
.split(['/', '|', '\n', ',', ',', '。', ';', ';'])
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
.take(4)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
|
||||
fn find_motion_template(id: &str) -> Option<&'static MotionTemplate> {
|
||||
pub(crate) fn find_motion_template(id: &str) -> Option<&'static MotionTemplate> {
|
||||
BUILT_IN_MOTION_TEMPLATES
|
||||
.iter()
|
||||
.find(|template| template.id == id.trim())
|
||||
}
|
||||
|
||||
fn resolve_character_animation_model(payload: &CharacterAnimationGenerateRequest) -> String {
|
||||
let candidate = match payload.strategy {
|
||||
CharacterAnimationStrategy::ImageSequence => payload.image_sequence_model.as_str(),
|
||||
@@ -3486,12 +3192,12 @@ fn character_animation_error_response(
|
||||
error.into_response_with_context(Some(request_context))
|
||||
}
|
||||
|
||||
struct MotionTemplate {
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
animation: &'static str,
|
||||
prompt_suffix: &'static str,
|
||||
notes: &'static str,
|
||||
pub(crate) struct MotionTemplate {
|
||||
pub(crate) id: &'static str,
|
||||
pub(crate) label: &'static str,
|
||||
pub(crate) animation: &'static str,
|
||||
pub(crate) prompt_suffix: &'static str,
|
||||
pub(crate) notes: &'static str,
|
||||
}
|
||||
|
||||
impl MotionTemplate {
|
||||
@@ -3677,6 +3383,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub(crate) fn find_motion_template(id: &str) -> Option<&'static MotionTemplate> {
|
||||
BUILT_IN_MOTION_TEMPLATES
|
||||
.iter()
|
||||
.find(|template| template.id == id.trim())
|
||||
}
|
||||
fn resolve_character_animation_model_uses_strategy_specific_field() {
|
||||
let payload = CharacterAnimationGenerateRequest {
|
||||
character_id: "hero".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user