This commit is contained in:
2026-04-24 22:27:45 +08:00
35 changed files with 1862 additions and 237 deletions

View File

@@ -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,