This commit is contained in:
2026-04-24 17:59:48 +08:00
parent 929febb4fe
commit 6cb3efae61
55 changed files with 2373 additions and 435 deletions

View File

@@ -48,6 +48,12 @@ const CHARACTER_VISUAL_ENTITY_KIND: &str = "character";
const CHARACTER_VISUAL_SLOT: &str = "primary_visual";
const CHARACTER_VISUAL_TASK_POLL_INTERVAL_MS: u64 = 2_500;
#[derive(Clone, Debug)]
pub(crate) struct GeneratedCharacterPrimaryVisual {
pub image_src: String,
pub asset_id: String,
}
pub async fn generate_character_visual(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -277,6 +283,90 @@ pub async fn generate_character_visual(
))
}
pub(crate) async fn generate_character_primary_visual_for_profile(
state: &AppState,
owner_user_id: &str,
character_id: &str,
prompt_text: &str,
character_brief_text: Option<&str>,
) -> Result<GeneratedCharacterPrimaryVisual, AppError> {
let payload = CharacterVisualGenerateRequest {
character_id: character_id.to_string(),
source_mode: shared_contracts::assets::CharacterVisualSourceMode::TextToImage,
prompt_text: prompt_text.to_string(),
character_brief_text: character_brief_text.map(ToOwned::to_owned),
reference_image_data_urls: Vec::new(),
candidate_count: 1,
image_model: CHARACTER_VISUAL_MODEL.to_string(),
size: "1024*1024".to_string(),
};
let task_id = generate_ai_task_id(current_utc_micros());
let prompt = build_character_visual_prompt(
payload.prompt_text.as_str(),
payload.character_brief_text.as_deref(),
);
let character_id = normalize_required_text(payload.character_id.as_str(), "character");
let model = normalize_required_text(payload.image_model.as_str(), CHARACTER_VISUAL_MODEL);
let size = normalize_required_text(payload.size.as_str(), "1024*1024");
create_visual_task(
state,
&task_id,
owner_user_id,
&character_id,
&model,
&prompt,
)?;
let settings = require_dashscope_settings(state)?;
let http_client = build_dashscope_http_client(&settings)?;
state
.ai_task_service()
.start_task(task_id.as_str(), current_utc_micros())
.map_err(map_ai_task_error)?;
let generated = create_character_visual_generation(
&http_client,
&settings,
model.as_str(),
prompt.as_str(),
size.as_str(),
1,
&[],
)
.await?;
let drafts = persist_visual_drafts(
state,
owner_user_id,
&character_id,
&task_id,
generated.images,
size.as_str(),
)
.await?;
let draft = drafts.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "character-visual",
"message": "角色主形象生成没有返回候选图。",
}))
})?;
let asset_id = format!("visual-{character_id}-{task_id}");
let image_src = persist_published_visual(
state,
owner_user_id,
&character_id,
asset_id.as_str(),
draft.image_src.as_str(),
Some(prompt.as_str()),
)
.await?;
state
.ai_task_service()
.complete_task(task_id.as_str(), current_utc_micros())
.map_err(map_ai_task_error)?;
Ok(GeneratedCharacterPrimaryVisual {
image_src,
asset_id,
})
}
pub async fn get_character_visual_job(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,