fix: stabilize rpg creation entry and opening cg
This commit is contained in:
@@ -10,7 +10,9 @@ use axum::{
|
||||
response::Response,
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use image::{DynamicImage, GenericImageView, imageops::FilterType};
|
||||
use image::{
|
||||
DynamicImage, GenericImageView, ImageFormat, codecs::jpeg::JpegEncoder, imageops::FilterType,
|
||||
};
|
||||
use module_assets::{
|
||||
AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input,
|
||||
build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id,
|
||||
@@ -375,6 +377,8 @@ const OPENING_CG_ENTITY_KIND: &str = "custom_world_profile";
|
||||
const OPENING_CG_STORYBOARD_SLOT: &str = "opening_cg_storyboard";
|
||||
const OPENING_CG_VIDEO_SLOT: &str = "opening_cg_video";
|
||||
const ARK_VIDEO_TASK_POLL_INTERVAL_MS: u64 = 5_000;
|
||||
const OPENING_CG_REFERENCE_MAX_EDGE: u32 = 768;
|
||||
const OPENING_CG_REFERENCE_JPEG_QUALITY: u8 = 82;
|
||||
|
||||
struct CoverPromptContext {
|
||||
opening_act_title: String,
|
||||
@@ -1025,6 +1029,16 @@ pub async fn generate_custom_world_opening_cg(
|
||||
"openingSceneImageSrc",
|
||||
)
|
||||
.await?;
|
||||
let player_role_reference = resize_image_reference_data_url(
|
||||
player_role_reference,
|
||||
OPENING_CG_REFERENCE_MAX_EDGE,
|
||||
OPENING_CG_REFERENCE_JPEG_QUALITY,
|
||||
)?;
|
||||
let opening_scene_reference = resize_image_reference_data_url(
|
||||
opening_scene_reference,
|
||||
OPENING_CG_REFERENCE_MAX_EDGE,
|
||||
OPENING_CG_REFERENCE_JPEG_QUALITY,
|
||||
)?;
|
||||
let storyboard = generate_opening_cg_storyboard(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
@@ -1617,6 +1631,52 @@ async fn resolve_reference_image_as_data_url(
|
||||
))
|
||||
}
|
||||
|
||||
fn resize_image_reference_data_url(
|
||||
data_url: String,
|
||||
max_edge: u32,
|
||||
jpeg_quality: u8,
|
||||
) -> Result<String, AppError> {
|
||||
if max_edge == 0 {
|
||||
return Ok(data_url);
|
||||
}
|
||||
let Some(parsed) = parse_image_data_url(data_url.as_str()) else {
|
||||
return Ok(data_url);
|
||||
};
|
||||
let image = image::load_from_memory(parsed.bytes.as_slice()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "custom-world-ai",
|
||||
"message": format!("无法解析参考图:{error}"),
|
||||
}))
|
||||
})?;
|
||||
let (width, height) = image.dimensions();
|
||||
let already_within_budget = width <= max_edge && height <= max_edge;
|
||||
if already_within_budget && parsed.mime_type == "image/jpeg" {
|
||||
return Ok(data_url);
|
||||
}
|
||||
|
||||
// 中文注释:开局 CG 故事板会同时带角色和场景两张参考图;先压到较小 JPEG,避免大图 PNG Data URL 让 VectorEngine 网关在请求发送阶段中断。
|
||||
let resized = if already_within_budget {
|
||||
image
|
||||
} else {
|
||||
image.resize(max_edge, max_edge, FilterType::Triangle)
|
||||
};
|
||||
let encoded_image = DynamicImage::ImageRgb8(resized.to_rgb8());
|
||||
let mut encoded = Vec::new();
|
||||
JpegEncoder::new_with_quality(&mut encoded, jpeg_quality)
|
||||
.encode_image(&encoded_image)
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": "custom-world-ai",
|
||||
"message": format!("压缩参考图失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
|
||||
Ok(format!(
|
||||
"data:image/jpeg;base64,{}",
|
||||
BASE64_STANDARD.encode(encoded)
|
||||
))
|
||||
}
|
||||
|
||||
async fn create_text_to_image_generation(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &DashScopeSettings,
|
||||
@@ -3065,6 +3125,34 @@ mod tests {
|
||||
assert_eq!(parsed.bytes, b"hello".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_cg_reference_data_url_is_resized_to_request_budget() {
|
||||
let image = DynamicImage::ImageRgb8(image::RgbImage::new(2048, 1152));
|
||||
let mut cursor = std::io::Cursor::new(Vec::new());
|
||||
image
|
||||
.write_to(&mut cursor, ImageFormat::Png)
|
||||
.expect("test image should encode");
|
||||
let data_url = format!(
|
||||
"data:image/png;base64,{}",
|
||||
BASE64_STANDARD.encode(cursor.into_inner())
|
||||
);
|
||||
|
||||
let resized = resize_image_reference_data_url(
|
||||
data_url,
|
||||
OPENING_CG_REFERENCE_MAX_EDGE,
|
||||
OPENING_CG_REFERENCE_JPEG_QUALITY,
|
||||
)
|
||||
.expect("reference should resize");
|
||||
let parsed = parse_image_data_url(resized.as_str()).expect("resized data url should parse");
|
||||
let resized_image =
|
||||
image::load_from_memory(parsed.bytes.as_slice()).expect("resized image should decode");
|
||||
let (width, height) = resized_image.dimensions();
|
||||
|
||||
assert!(width <= OPENING_CG_REFERENCE_MAX_EDGE);
|
||||
assert!(height <= OPENING_CG_REFERENCE_MAX_EDGE);
|
||||
assert_eq!(parsed.mime_type, "image/jpeg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_cover_reference_source_keeps_full_data_url() {
|
||||
let mut sources = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user