1
This commit is contained in:
@@ -38,7 +38,8 @@ use spacetime_client::{
|
||||
CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord,
|
||||
CustomWorldWorkSummaryRecord, SpacetimeClientError,
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
use std::{collections::BTreeSet, convert::Infallible, sync::Arc};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::info;
|
||||
|
||||
@@ -65,6 +66,7 @@ use crate::{
|
||||
};
|
||||
|
||||
const DRAFT_ASSET_GENERATION_MAX_ATTEMPTS: u32 = 3;
|
||||
const DRAFT_ASSET_GENERATION_MAX_CONCURRENT_REQUESTS: usize = 2;
|
||||
|
||||
pub async fn get_custom_world_library(
|
||||
State(state): State<AppState>,
|
||||
@@ -1226,9 +1228,12 @@ fn spawn_custom_world_draft_foundation_job(
|
||||
}
|
||||
};
|
||||
|
||||
let image_generation_limiter = Arc::new(Semaphore::new(
|
||||
DRAFT_ASSET_GENERATION_MAX_CONCURRENT_REQUESTS,
|
||||
));
|
||||
let role_visual_profile_input = draft_profile_value.clone();
|
||||
let act_background_profile_input = draft_profile_value.clone();
|
||||
// 角色主形象与幕背景图互不依赖,必须并行发起,避免底稿阶段串行等待两类图片。
|
||||
// 角色主形象与幕背景图互不依赖,必须并行发起;上游生图请求统一限流,避免同批草稿瞬时打满供应商接口。
|
||||
let (role_visual_result, act_background_result) = tokio::join!(
|
||||
async {
|
||||
let mut profile = role_visual_profile_input;
|
||||
@@ -1238,6 +1243,7 @@ fn spawn_custom_world_draft_foundation_job(
|
||||
&owner_user_id,
|
||||
&operation_id,
|
||||
&mut profile,
|
||||
image_generation_limiter.clone(),
|
||||
)
|
||||
.await
|
||||
.map(|_| profile)
|
||||
@@ -1250,6 +1256,7 @@ fn spawn_custom_world_draft_foundation_job(
|
||||
&owner_user_id,
|
||||
&operation_id,
|
||||
&mut profile,
|
||||
image_generation_limiter.clone(),
|
||||
)
|
||||
.await
|
||||
.map(|_| profile)
|
||||
@@ -1386,6 +1393,7 @@ async fn generate_draft_foundation_role_visuals(
|
||||
owner_user_id: &str,
|
||||
operation_id: &str,
|
||||
draft_profile: &mut Value,
|
||||
image_generation_limiter: Arc<Semaphore>,
|
||||
) -> Result<(), String> {
|
||||
let Some(profile_object) = draft_profile.as_object_mut() else {
|
||||
return Err("foundation draft JSON 必须是 object".to_string());
|
||||
@@ -1439,17 +1447,25 @@ async fn generate_draft_foundation_role_visuals(
|
||||
for role_ref in role_generation_refs {
|
||||
let task_state = (*state).clone();
|
||||
let task_owner_user_id = owner_user_id.to_string();
|
||||
let task_limiter = image_generation_limiter.clone();
|
||||
generation_tasks.spawn(async move {
|
||||
let mut last_error = None;
|
||||
for attempt in 1..=DRAFT_ASSET_GENERATION_MAX_ATTEMPTS {
|
||||
match generate_character_primary_visual_for_profile(
|
||||
&task_state,
|
||||
task_owner_user_id.as_str(),
|
||||
role_ref.role_id.as_str(),
|
||||
role_ref.prompt.as_str(),
|
||||
Some(role_ref.name.as_str()),
|
||||
)
|
||||
.await
|
||||
let generation_result = {
|
||||
let _permit = task_limiter
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|error| format!("图片生成并发控制失效:{error}"))?;
|
||||
generate_character_primary_visual_for_profile(
|
||||
&task_state,
|
||||
task_owner_user_id.as_str(),
|
||||
role_ref.role_id.as_str(),
|
||||
role_ref.prompt.as_str(),
|
||||
Some(role_ref.name.as_str()),
|
||||
)
|
||||
.await
|
||||
};
|
||||
match generation_result
|
||||
{
|
||||
Ok(generated) => {
|
||||
return Ok::<_, String>((role_ref.key, role_ref.index, generated));
|
||||
@@ -1499,7 +1515,7 @@ async fn generate_draft_foundation_role_visuals(
|
||||
}
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(errors.join(";"));
|
||||
return Err(join_unique_error_messages(errors));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1510,6 +1526,7 @@ async fn generate_draft_foundation_act_backgrounds(
|
||||
owner_user_id: &str,
|
||||
operation_id: &str,
|
||||
draft_profile: &mut Value,
|
||||
image_generation_limiter: Arc<Semaphore>,
|
||||
) -> Result<(), String> {
|
||||
let world_name =
|
||||
json_text_from_value(draft_profile, "name").unwrap_or_else(|| "未命名世界".to_string());
|
||||
@@ -1536,20 +1553,28 @@ async fn generate_draft_foundation_act_backgrounds(
|
||||
let task_owner_user_id = owner_user_id.to_string();
|
||||
let task_profile_id = profile_id.clone();
|
||||
let task_world_name = world_name.clone();
|
||||
let task_limiter = image_generation_limiter.clone();
|
||||
generation_tasks.spawn(async move {
|
||||
let mut last_error = None;
|
||||
for attempt in 1..=DRAFT_ASSET_GENERATION_MAX_ATTEMPTS {
|
||||
match generate_custom_world_scene_image_for_profile(
|
||||
&task_state,
|
||||
task_owner_user_id.as_str(),
|
||||
task_profile_id.as_deref(),
|
||||
task_world_name.as_str(),
|
||||
act_ref.scene_id.as_str(),
|
||||
act_ref.title.as_str(),
|
||||
act_ref.summary.as_str(),
|
||||
act_ref.prompt.as_str(),
|
||||
)
|
||||
.await
|
||||
let generation_result = {
|
||||
let _permit = task_limiter
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|error| format!("图片生成并发控制失效:{error}"))?;
|
||||
generate_custom_world_scene_image_for_profile(
|
||||
&task_state,
|
||||
task_owner_user_id.as_str(),
|
||||
task_profile_id.as_deref(),
|
||||
task_world_name.as_str(),
|
||||
act_ref.scene_id.as_str(),
|
||||
act_ref.title.as_str(),
|
||||
act_ref.summary.as_str(),
|
||||
act_ref.prompt.as_str(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
match generation_result
|
||||
{
|
||||
Ok(generated) => {
|
||||
return Ok::<_, String>((
|
||||
@@ -1571,7 +1596,9 @@ async fn generate_draft_foundation_act_backgrounds(
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"幕「{}」背景图连续生成 {} 次失败:{}",
|
||||
"第{}章第{}幕「{}」背景图连续生成 {} 次失败:{}",
|
||||
act_ref.chapter_index + 1,
|
||||
act_ref.act_index + 1,
|
||||
act_ref.title,
|
||||
DRAFT_ASSET_GENERATION_MAX_ATTEMPTS,
|
||||
last_error.unwrap_or_else(|| "未知错误".to_string())
|
||||
@@ -1617,11 +1644,23 @@ async fn generate_draft_foundation_act_backgrounds(
|
||||
}
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(errors.join(";"));
|
||||
return Err(join_unique_error_messages(errors));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn join_unique_error_messages(messages: Vec<String>) -> String {
|
||||
// 并行图片任务可能从同一个上游故障返回完全相同的业务错误;用户侧只需要看到去重后的失败项。
|
||||
messages
|
||||
.into_iter()
|
||||
.map(|message| message.trim().to_string())
|
||||
.filter(|message| !message.is_empty())
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
}
|
||||
|
||||
struct RoleVisualGenerationRef {
|
||||
key: String,
|
||||
index: usize,
|
||||
|
||||
Reference in New Issue
Block a user