This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -37,8 +37,9 @@ use shared_contracts::assets::{
CharacterAnimationImportVideoResponse, CharacterAnimationPublishRequest,
CharacterAnimationPublishResponse, CharacterAnimationStrategy,
CharacterAnimationTemplatePayload, CharacterAnimationTemplatesResponse,
CharacterAssetJobStatusPayload, CharacterAssetJobStatusText, CharacterVisualDraftPayload,
CharacterWorkflowCacheGetResponse, CharacterWorkflowCachePayload,
CharacterAssetJobStatusPayload, CharacterAssetJobStatusText,
CharacterRoleAssetWorkflowResolveRequest, CharacterRoleAssetWorkflowResponse,
CharacterVisualDraftPayload, CharacterWorkflowCacheGetResponse, CharacterWorkflowCachePayload,
CharacterWorkflowCacheSaveRequest, CharacterWorkflowCacheSaveResponse,
};
use spacetime_client::SpacetimeClientError;
@@ -49,6 +50,9 @@ use crate::{
build_character_animation_prompt, build_fallback_moderation_safe_animation_prompt,
},
http_error::AppError,
prompt::role_asset_studio::{
build_role_asset_workflow, normalize_animation_prompt_text_by_key,
},
request_context::RequestContext,
state::AppState,
};
@@ -646,6 +650,92 @@ pub async fn save_character_workflow_cache(
))
}
pub async fn resolve_role_asset_workflow(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
AxumPath(character_id): AxumPath<String>,
payload: Result<Json<CharacterRoleAssetWorkflowResolveRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let character_id = normalize_required_text(character_id.as_str(), "");
if character_id.is_empty() {
return Err(character_animation_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "role-asset-workflow",
"message": "characterId is required.",
})),
));
}
let Json(payload) = payload.map_err(|error| {
character_animation_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "role-asset-workflow",
"message": error.body_text(),
})),
)
})?;
let cache_scope_id = trim_optional_text(payload.cache_scope_id.as_deref());
let cache = load_workflow_cache(&state, character_id.as_str(), cache_scope_id.as_deref())
.await
.map_err(|error| character_animation_error_response(&request_context, error))?;
let workflow = build_role_asset_workflow(payload.role, cache.as_ref());
Ok(json_success_body(
Some(&request_context),
CharacterRoleAssetWorkflowResponse {
ok: true,
cache,
workflow,
},
))
}
pub async fn put_role_asset_workflow(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
AxumPath(character_id): AxumPath<String>,
payload: Result<Json<CharacterWorkflowCacheSaveRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let character_id = normalize_required_text(character_id.as_str(), "");
if character_id.is_empty() {
return Err(character_animation_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "role-asset-workflow",
"message": "characterId is required.",
})),
));
}
let Json(mut payload) = payload.map_err(|error| {
character_animation_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "role-asset-workflow",
"message": error.body_text(),
})),
)
})?;
payload.character_id = character_id;
let cache = normalize_workflow_cache_payload(payload, current_utc_iso_text());
save_workflow_cache(&state, cache.clone())
.await
.map_err(|error| character_animation_error_response(&request_context, error))?;
Ok(json_success_body(
Some(&request_context),
CharacterWorkflowCacheSaveResponse {
ok: true,
cache,
save_message: "角色资产工坊缓存已更新到 OSS。".to_string(),
},
))
}
fn create_animation_task(
state: &AppState,
task_id: &str,
@@ -1634,6 +1724,9 @@ fn normalize_workflow_cache_payload(
cache_scope_id,
visual_prompt_text: clamp_prompt_seed_text(payload.visual_prompt_text.as_deref()),
animation_prompt_text: clamp_prompt_seed_text(payload.animation_prompt_text.as_deref()),
animation_prompt_text_by_key: normalize_animation_prompt_text_by_key(
payload.animation_prompt_text_by_key,
),
visual_drafts: normalize_visual_drafts(character_id.as_str(), payload.visual_drafts),
selected_visual_draft_id: trim_optional_text(payload.selected_visual_draft_id.as_deref())
.unwrap_or_default(),
@@ -3354,6 +3447,10 @@ mod tests {
cache_scope_id: None,
visual_prompt_text: Some("主形象".to_string()),
animation_prompt_text: Some("待机".to_string()),
animation_prompt_text_by_key: BTreeMap::from([(
"run".to_string(),
"奔跑".to_string(),
)]),
visual_drafts: vec![CharacterVisualDraftPayload {
id: "".to_string(),
label: "".to_string(),
@@ -3373,6 +3470,7 @@ mod tests {
assert_eq!(cache.character_id, "hero");
assert_eq!(cache.selected_animation, "idle");
assert_eq!(cache.animation_prompt_text_by_key["run"], "奔跑");
assert_eq!(cache.visual_drafts[0].id, "hero-draft-1");
assert_eq!(cache.visual_drafts[0].width, 1024);
assert_eq!(cache.image_src, None);