This commit is contained in:
2026-05-03 00:17:50 +08:00
parent 5831703156
commit 801d1d534a
16 changed files with 1337 additions and 449 deletions

View File

@@ -36,6 +36,10 @@ use crate::{
},
http_error::AppError,
llm_model_routing::CREATION_TEMPLATE_LLM_MODEL,
openai_image_generation::{
DownloadedOpenAiImage, GPT_IMAGE_2_MODEL, build_openai_image_http_client,
create_openai_image_generation, require_openai_image_settings,
},
platform_errors::map_oss_error,
prompt::scene_background::{
DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT, SceneImagePromptLandmark,
@@ -312,6 +316,8 @@ struct DownloadedRemoteImage {
bytes: Vec<u8>,
}
const RPG_SCENE_IMAGE_MODEL: &str = GPT_IMAGE_2_MODEL;
struct CoverPromptContext {
opening_act_title: String,
opening_act_summary: String,
@@ -443,7 +449,7 @@ pub async fn generate_custom_world_scene_image(
let owner_user_id = authenticated.claims().user_id().to_string();
let normalized = normalize_scene_image_request(payload)
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
require_dashscope_settings(&state)
require_openai_image_settings(&state)
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
let asset_id = format!("custom-scene-{}", current_utc_millis());
let asset = execute_billable_asset_operation(
@@ -452,8 +458,8 @@ pub async fn generate_custom_world_scene_image(
"scene_image",
asset_id.as_str(),
async {
let settings = require_dashscope_settings(&state)?;
let http_client = build_dashscope_http_client(&settings)?;
let settings = require_openai_image_settings(&state)?;
let http_client = build_openai_image_http_client(&settings)?;
let reference_image =
if let Some(reference_image_src) = normalized.reference_image_src.as_deref() {
Some(
@@ -468,46 +474,32 @@ pub async fn generate_custom_world_scene_image(
} else {
None
};
let generated = if let Some(reference_image) = reference_image.as_deref() {
create_reference_image_generation(
&http_client,
&settings,
state.config.dashscope_reference_image_model.as_str(),
normalized.prompt.as_str(),
normalized.size.as_str(),
&[reference_image.to_string()],
Some(normalized.negative_prompt.as_str()),
"创建参考图场景编辑任务失败",
"参考图场景编辑未返回图片地址",
"scene-edit",
)
.await
} else {
create_text_to_image_generation(
&http_client,
&settings,
state.config.dashscope_scene_image_model.as_str(),
normalized.prompt.as_str(),
Some(normalized.negative_prompt.as_str()),
normalized.size.as_str(),
"创建场景图片生成任务失败",
"查询场景图片任务失败",
"场景图片生成任务失败",
"场景图片生成超时或未返回图片地址",
)
.await
}?;
let scene_model = if reference_image.is_some() {
state.config.dashscope_reference_image_model.clone()
} else {
state.config.dashscope_scene_image_model.clone()
};
let downloaded = download_remote_image(
let reference_images = reference_image
.as_ref()
.map(|value| vec![value.clone()])
.unwrap_or_default();
let generated = create_openai_image_generation(
&http_client,
generated.image_url.as_str(),
"下载生成图片失败",
&settings,
normalized.prompt.as_str(),
Some(normalized.negative_prompt.as_str()),
normalized.size.as_str(),
1,
&reference_images,
"场景图片生成失败",
)
.await?;
let downloaded = generated
.images
.into_iter()
.next()
.map(downloaded_openai_to_custom_world_image)
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "apimart",
"message": "场景图片生成成功但未返回图片。",
}))
})?;
let upload = PreparedAssetUpload {
prefix: LegacyAssetPrefix::CustomWorldScenes,
path_segments: vec![
@@ -539,7 +531,7 @@ pub async fn generate_custom_world_scene_image(
image_src: String::new(),
asset_id: asset_id.clone(),
source_type: "generated".to_string(),
model: Some(scene_model),
model: Some(RPG_SCENE_IMAGE_MODEL.to_string()),
size: Some(normalized.size),
task_id: Some(generated.task_id),
prompt: Some(normalized.prompt),
@@ -588,27 +580,30 @@ pub(crate) async fn generate_custom_world_scene_image_for_profile(
}),
};
let normalized = normalize_scene_image_request(payload)?;
let settings = require_dashscope_settings(state)?;
let http_client = build_dashscope_http_client(&settings)?;
let generated = create_text_to_image_generation(
let settings = require_openai_image_settings(state)?;
let http_client = build_openai_image_http_client(&settings)?;
let generated = create_openai_image_generation(
&http_client,
&settings,
state.config.dashscope_scene_image_model.as_str(),
normalized.prompt.as_str(),
Some(normalized.negative_prompt.as_str()),
normalized.size.as_str(),
"创建场景图片生成任务失败",
"查询场景图片任务失败",
"场景图片生成任务失败",
"场景图片生成超时或未返回图片地址",
)
.await?;
let downloaded = download_remote_image(
&http_client,
generated.image_url.as_str(),
"下载生成图片失败",
1,
&[],
"场景图片生成失败",
)
.await?;
let downloaded = generated
.images
.into_iter()
.next()
.map(downloaded_openai_to_custom_world_image)
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "apimart",
"message": "场景图片生成成功但未返回图片。",
}))
})?;
let asset_id = format!("custom-scene-{}", current_utc_millis());
let upload = PreparedAssetUpload {
prefix: LegacyAssetPrefix::CustomWorldScenes,
@@ -633,7 +628,7 @@ pub(crate) async fn generate_custom_world_scene_image_for_profile(
slot: "scene_image",
source_job_id: Some(generated.task_id.clone()),
};
let model = state.config.dashscope_scene_image_model.clone();
let model = RPG_SCENE_IMAGE_MODEL.to_string();
let prompt = normalized.prompt.clone();
let asset = persist_custom_world_asset(
state,
@@ -1634,6 +1629,14 @@ async fn download_remote_image(
})
}
fn downloaded_openai_to_custom_world_image(image: DownloadedOpenAiImage) -> DownloadedRemoteImage {
DownloadedRemoteImage {
extension: image.extension,
mime_type: image.mime_type,
bytes: image.bytes,
}
}
fn optimize_uploaded_cover_image(
parsed_data_url: &ParsedImageDataUrl,
crop_rect: &CustomWorldCoverCropRect,
@@ -2451,6 +2454,12 @@ mod tests {
serde_json::from_slice(&body).expect("body should be valid json")
}
fn build_state_without_apimart_key() -> AppState {
let mut config = AppConfig::default();
config.apimart_api_key = None;
AppState::new(config).expect("state should build")
}
fn build_state_without_dashscope_key() -> AppState {
let mut config = AppConfig::default();
config.dashscope_api_key = None;
@@ -2458,8 +2467,8 @@ mod tests {
}
#[tokio::test]
async fn scene_image_returns_service_unavailable_when_dashscope_missing() {
let state = build_state_without_dashscope_key();
async fn scene_image_returns_service_unavailable_when_apimart_missing() {
let state = build_state_without_apimart_key();
let request_context = build_request_context("POST /api/runtime/custom-world/scene-image");
let authenticated = build_authenticated(&state);
@@ -2482,7 +2491,7 @@ mod tests {
})),
)
.await
.expect_err("missing dashscope should fail");
.expect_err("missing apimart should fail");
let payload = read_error_response(response).await;
assert_eq!(
@@ -2491,7 +2500,7 @@ mod tests {
);
assert_eq!(
payload["error"]["details"]["provider"],
Value::String("dashscope".to_string())
Value::String("apimart".to_string())
);
}
@@ -2612,6 +2621,11 @@ mod tests {
assert_eq!(normalized.prompt, manual_prompt);
}
#[test]
fn scene_image_response_model_is_gpt_image_2() {
assert_eq!(RPG_SCENE_IMAGE_MODEL, "gpt-image-2");
}
#[tokio::test]
async fn cover_image_returns_service_unavailable_when_dashscope_missing() {
let state = build_state_without_dashscope_key();