feat: 完善敲木鱼玩法模板链路
This commit is contained in:
@@ -42,9 +42,6 @@ use crate::{
|
||||
platform_errors::map_oss_error,
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
vector_engine_audio_generation::{
|
||||
GeneratedCreationAudioTarget, generate_sound_effect_asset_for_creation,
|
||||
},
|
||||
};
|
||||
|
||||
const WOODEN_FISH_PROVIDER: &str = "wooden-fish";
|
||||
@@ -53,16 +50,17 @@ const WOODEN_FISH_RUNTIME_PROVIDER: &str = "wooden-fish-runtime";
|
||||
const WOODEN_FISH_TEMPLATE_ID: &str = "wooden-fish";
|
||||
const WOODEN_FISH_TEMPLATE_NAME: &str = "敲木鱼";
|
||||
const DEFAULT_HIT_OBJECT_PROMPT: &str = "默认敲击物图案,圆润木质质感,透明背景";
|
||||
const DEFAULT_HIT_SOUND_PROMPT: &str = "清脆短促的木鱼敲击声";
|
||||
const DEFAULT_HIT_OBJECT_ASSET_ID: &str = "wooden-fish-default-hit-object";
|
||||
const DEFAULT_HIT_OBJECT_IMAGE_SRC: &str = "/wooden-fish/default-hit-object.png";
|
||||
const DEFAULT_HIT_SOUND_ASSET_ID: &str = "wooden-fish-default-hit-sound";
|
||||
const DEFAULT_HIT_SOUND_AUDIO_SRC: &str = "/wooden-fish/default-hit-sound.mp3";
|
||||
const WOODEN_FISH_ENTITY_KIND: &str = "wooden_fish_work";
|
||||
const WOODEN_FISH_HIT_OBJECT_SLOT: &str = "hit_object";
|
||||
const WOODEN_FISH_HIT_OBJECT_ASSET_KIND: &str = "wooden_fish_hit_object";
|
||||
const WOODEN_FISH_BACKGROUND_SLOT: &str = "background";
|
||||
const WOODEN_FISH_BACKGROUND_ASSET_KIND: &str = "wooden_fish_background";
|
||||
const WOODEN_FISH_HIT_SOUND_SLOT: &str = "hit_sound";
|
||||
const WOODEN_FISH_HIT_SOUND_ASSET_KIND: &str = "wooden_fish_hit_sound";
|
||||
const WOODEN_FISH_BACK_BUTTON_SLOT: &str = "back_button";
|
||||
const WOODEN_FISH_BACK_BUTTON_ASSET_KIND: &str = "wooden_fish_back_button";
|
||||
const WOODEN_FISH_HIT_SOUND_DURATION_SECONDS: u8 = 3;
|
||||
const DEFAULT_HIT_OBJECT_REFERENCE_BYTES: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
@@ -154,14 +152,7 @@ pub async fn execute_wooden_fish_action(
|
||||
&mut payload,
|
||||
)
|
||||
.await?;
|
||||
maybe_generate_hit_sound_asset(
|
||||
&state,
|
||||
&request_context,
|
||||
&session_id,
|
||||
owner_user_id.as_str(),
|
||||
&mut payload,
|
||||
)
|
||||
.await?;
|
||||
maybe_generate_hit_sound_asset(&mut payload);
|
||||
let response = state
|
||||
.spacetime_client()
|
||||
.execute_wooden_fish_action(session_id, owner_user_id, payload)
|
||||
@@ -371,16 +362,15 @@ fn build_wooden_fish_draft(payload: &WoodenFishWorkspaceCreateRequest) -> Wooden
|
||||
.as_ref()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty()),
|
||||
hit_sound_prompt: payload
|
||||
.hit_sound_prompt
|
||||
.as_ref()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.or_else(|| Some(DEFAULT_HIT_SOUND_PROMPT.to_string())),
|
||||
hit_sound_prompt: None,
|
||||
floating_words: normalize_floating_words(payload.floating_words.clone()),
|
||||
hit_object_asset: None,
|
||||
background_asset: None,
|
||||
hit_sound_asset: payload.hit_sound_asset.clone(),
|
||||
back_button_asset: None,
|
||||
hit_sound_asset: payload
|
||||
.hit_sound_asset
|
||||
.clone()
|
||||
.or_else(|| Some(default_wooden_fish_hit_sound_asset())),
|
||||
cover_image_src: None,
|
||||
generation_status: WoodenFishGenerationStatus::Draft,
|
||||
}
|
||||
@@ -418,7 +408,10 @@ async fn maybe_generate_hit_object_asset(
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
if payload.hit_object_asset.is_some() && payload.background_asset.is_some() {
|
||||
if payload.hit_object_asset.is_some()
|
||||
&& payload.background_asset.is_some()
|
||||
&& payload.back_button_asset.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -447,6 +440,7 @@ async fn maybe_generate_hit_object_asset(
|
||||
})?;
|
||||
payload.hit_object_asset = Some(generated.hit_object_asset);
|
||||
payload.background_asset = Some(generated.background_asset);
|
||||
payload.back_button_asset = Some(generated.back_button_asset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -463,6 +457,18 @@ fn default_wooden_fish_hit_object_asset() -> WoodenFishImageAsset {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_wooden_fish_hit_sound_asset() -> WoodenFishAudioAsset {
|
||||
WoodenFishAudioAsset {
|
||||
asset_id: DEFAULT_HIT_SOUND_ASSET_ID.to_string(),
|
||||
audio_src: DEFAULT_HIT_SOUND_AUDIO_SRC.to_string(),
|
||||
audio_object_key: "public/wooden-fish/default-hit-sound.mp3".to_string(),
|
||||
asset_object_id: DEFAULT_HIT_SOUND_ASSET_ID.to_string(),
|
||||
source: "bundled-default".to_string(),
|
||||
prompt: Some("默认木鱼音".to_string()),
|
||||
duration_ms: Some(u32::from(WOODEN_FISH_HIT_SOUND_DURATION_SECONDS) * 1_000),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_default_hit_object_prompt(prompt: &str) -> bool {
|
||||
let normalized = normalize_hit_object_prompt_for_default_match(prompt);
|
||||
normalized.is_empty()
|
||||
@@ -530,130 +536,27 @@ async fn resolve_hit_object_profile_id(
|
||||
})
|
||||
}
|
||||
|
||||
async fn maybe_generate_hit_sound_asset(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
session_id: &str,
|
||||
owner_user_id: &str,
|
||||
payload: &mut WoodenFishActionRequest,
|
||||
) -> Result<(), Response> {
|
||||
fn maybe_generate_hit_sound_asset(payload: &mut WoodenFishActionRequest) {
|
||||
if !matches!(
|
||||
payload.action_type,
|
||||
shared_contracts::wooden_fish::WoodenFishActionType::CompileDraft
|
||||
| shared_contracts::wooden_fish::WoodenFishActionType::GenerateHitSound
|
||||
| shared_contracts::wooden_fish::WoodenFishActionType::ReplaceHitSound
|
||||
) {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
if matches!(
|
||||
payload.action_type,
|
||||
shared_contracts::wooden_fish::WoodenFishActionType::CompileDraft
|
||||
) && payload.hit_sound_asset.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
payload.hit_sound_prompt = None;
|
||||
if payload.hit_sound_asset.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let profile_id =
|
||||
resolve_hit_object_profile_id(state, request_context, session_id, owner_user_id, payload)
|
||||
.await?;
|
||||
payload.profile_id = Some(profile_id.clone());
|
||||
let prompt = payload
|
||||
.hit_sound_prompt
|
||||
.as_deref()
|
||||
.map(|value| clean_string(value, DEFAULT_HIT_SOUND_PROMPT))
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| DEFAULT_HIT_SOUND_PROMPT.to_string());
|
||||
|
||||
let asset = generate_wooden_fish_hit_sound_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
profile_id.as_str(),
|
||||
prompt.as_str(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
wooden_fish_error_response(request_context, WOODEN_FISH_CREATION_PROVIDER, error)
|
||||
})?;
|
||||
payload.hit_sound_asset = Some(asset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_wooden_fish_hit_sound_asset(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
profile_id: &str,
|
||||
prompt: &str,
|
||||
) -> Result<WoodenFishAudioAsset, AppError> {
|
||||
let final_prompt = build_wooden_fish_hit_sound_prompt(prompt);
|
||||
let generated = generate_sound_effect_asset_for_creation(
|
||||
state,
|
||||
owner_user_id,
|
||||
final_prompt.clone(),
|
||||
Some(WOODEN_FISH_HIT_SOUND_DURATION_SECONDS),
|
||||
None,
|
||||
GeneratedCreationAudioTarget {
|
||||
entity_kind: WOODEN_FISH_ENTITY_KIND.to_string(),
|
||||
entity_id: profile_id.to_string(),
|
||||
slot: WOODEN_FISH_HIT_SOUND_SLOT.to_string(),
|
||||
asset_kind: WOODEN_FISH_HIT_SOUND_ASSET_KIND.to_string(),
|
||||
profile_id: Some(profile_id.to_string()),
|
||||
storage_prefix: LegacyAssetPrefix::WoodenFishAssets,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
map_generated_creation_audio_to_wooden_fish_asset(
|
||||
profile_id,
|
||||
final_prompt.as_str(),
|
||||
generated,
|
||||
WOODEN_FISH_HIT_SOUND_DURATION_SECONDS,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_wooden_fish_hit_sound_prompt(prompt: &str) -> String {
|
||||
format!(
|
||||
"为敲木鱼玩法生成一次点击触发的短促敲击音效:{}。要求:干净、清脆、无旋律、无环境噪声、无语音、无文字提示音,适合高频点击时叠加播放。",
|
||||
clean_string(prompt, DEFAULT_HIT_SOUND_PROMPT)
|
||||
)
|
||||
}
|
||||
|
||||
fn map_generated_creation_audio_to_wooden_fish_asset(
|
||||
profile_id: &str,
|
||||
prompt: &str,
|
||||
asset: shared_contracts::creation_audio::CreationAudioAsset,
|
||||
duration_seconds: u8,
|
||||
) -> Result<WoodenFishAudioAsset, AppError> {
|
||||
let asset_object_id = asset
|
||||
.asset_object_id
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "敲木鱼音效生成完成但缺少资产对象 ID",
|
||||
}))
|
||||
})?;
|
||||
let audio_object_key = asset.audio_src.trim().trim_start_matches('/').to_string();
|
||||
if audio_object_key.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "敲木鱼音效生成完成但缺少音频地址",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(WoodenFishAudioAsset {
|
||||
asset_id: format!("{profile_id}-hit-sound-{}", asset.task_id),
|
||||
audio_src: asset.audio_src,
|
||||
audio_object_key,
|
||||
asset_object_id,
|
||||
source: "generated".to_string(),
|
||||
prompt: asset.prompt.or_else(|| Some(prompt.to_string())),
|
||||
duration_ms: Some(u32::from(duration_seconds) * 1_000),
|
||||
})
|
||||
payload.hit_sound_asset = Some(default_wooden_fish_hit_sound_asset());
|
||||
}
|
||||
|
||||
struct WoodenFishGeneratedImageAssets {
|
||||
hit_object_asset: WoodenFishImageAsset,
|
||||
background_asset: WoodenFishImageAsset,
|
||||
back_button_asset: WoodenFishImageAsset,
|
||||
}
|
||||
|
||||
async fn generate_wooden_fish_image_assets(
|
||||
@@ -674,7 +577,7 @@ async fn generate_wooden_fish_image_assets(
|
||||
let theme_reference_image =
|
||||
resolve_wooden_fish_theme_reference_image(clean_reference_image_src)?;
|
||||
|
||||
let (hit_object_asset, background_reference_image) =
|
||||
let (hit_object_asset, hit_object_reference_image) =
|
||||
if should_generate_wooden_fish_hit_object(prompt, clean_reference_image_src) {
|
||||
let hit_object_prompt = build_wooden_fish_hit_object_prompt(theme.as_str());
|
||||
let mut reference_images = vec![default_reference_image.clone()];
|
||||
@@ -699,8 +602,11 @@ async fn generate_wooden_fish_image_assets(
|
||||
"message": "生成敲木鱼敲击物图案失败:上游未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let background_reference_image =
|
||||
downloaded_wooden_fish_reference_image(&image, "wooden-fish-generated-hit-object");
|
||||
let image = prepare_wooden_fish_hit_object_image_for_persist(image)?;
|
||||
let hit_object_reference_image = downloaded_wooden_fish_reference_image(
|
||||
&image,
|
||||
"wooden-fish-generated-hit-object-transparent",
|
||||
);
|
||||
let hit_object_asset = persist_wooden_fish_image_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
@@ -719,7 +625,7 @@ async fn generate_wooden_fish_image_assets(
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
(hit_object_asset, background_reference_image)
|
||||
(hit_object_asset, hit_object_reference_image)
|
||||
} else {
|
||||
(
|
||||
default_wooden_fish_hit_object_asset(),
|
||||
@@ -734,7 +640,7 @@ async fn generate_wooden_fish_image_assets(
|
||||
background_prompt.as_str(),
|
||||
None,
|
||||
"9:16",
|
||||
&background_reference_image,
|
||||
&hit_object_reference_image,
|
||||
"生成敲木鱼背景环境图失败",
|
||||
)
|
||||
.await?;
|
||||
@@ -749,6 +655,8 @@ async fn generate_wooden_fish_image_assets(
|
||||
"message": "生成敲木鱼背景环境图失败:上游未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let background_reference_image =
|
||||
downloaded_wooden_fish_reference_image(&background_image, "wooden-fish-generated-background");
|
||||
let background_asset = persist_wooden_fish_image_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
@@ -767,23 +675,79 @@ async fn generate_wooden_fish_image_assets(
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let back_button_prompt = build_wooden_fish_back_button_prompt(theme.as_str());
|
||||
let back_button_generated = create_openai_image_edit_with_references(
|
||||
&http_client,
|
||||
&settings,
|
||||
back_button_prompt.as_str(),
|
||||
None,
|
||||
"1:1",
|
||||
1,
|
||||
&[
|
||||
hit_object_reference_image.clone(),
|
||||
background_reference_image,
|
||||
],
|
||||
"生成敲木鱼返回按钮图失败",
|
||||
)
|
||||
.await?;
|
||||
let back_button_task_id = back_button_generated.task_id.clone();
|
||||
let back_button_image = back_button_generated
|
||||
.images
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "生成敲木鱼返回按钮图失败:上游未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let back_button_image = prepare_wooden_fish_green_screen_image_for_persist(
|
||||
back_button_image,
|
||||
"敲木鱼返回按钮图",
|
||||
)?;
|
||||
let back_button_asset = persist_wooden_fish_image_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
back_button_task_id.as_str(),
|
||||
back_button_prompt.as_str(),
|
||||
back_button_image,
|
||||
current_utc_micros(),
|
||||
WoodenFishImageSlotPersistSpec {
|
||||
slot: WOODEN_FISH_BACK_BUTTON_SLOT,
|
||||
asset_kind: WOODEN_FISH_BACK_BUTTON_ASSET_KIND,
|
||||
asset_id_part: "back-button",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(WoodenFishGeneratedImageAssets {
|
||||
hit_object_asset,
|
||||
background_asset,
|
||||
back_button_asset,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_wooden_fish_hit_object_prompt(prompt: &str) -> String {
|
||||
format!(
|
||||
"生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。\n新主题为:{}",
|
||||
"生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。尺寸1:1,先输出绿色背景主体图(纯绿色绿幕),背景必须是单一纯绿色 #00FF00 且平整无纹理、无渐变、无阴影、无道具,主体完整居中,主体边缘必须干净,不要直接输出透明底。随后由服务端对绿色背景主体图做抠图去除绿色背景。最终结果只保留单个敲击物图案,禁止黑底、白底、棋盘格、纸板底或任何实底背景;主体本身不要使用与绿幕接近的纯绿色,若新主题天然包含绿色,请改用偏深、偏黄或偏蓝的绿色并与绿幕清晰区分。\n新主题为:{}",
|
||||
clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT)
|
||||
)
|
||||
}
|
||||
|
||||
fn build_wooden_fish_background_prompt(prompt: &str) -> String {
|
||||
format!(
|
||||
"生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。\n主题为:{}",
|
||||
"生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。尺寸竖屏9:16。参考图必须是第一步敲击物抠图完成后的透明图,不继承任何绿色底色、绿幕底色或纯绿色画布,并要求最终输出完整不透明的背景环境图。中央主体预留区必须保持干净,画面中央 40% 区域禁止出现主题主体、主体局部特写、主体轮廓影子、重复元素或主题主体的局部碎片;主题元素只允许出现在外围氛围,不得把主题物品画在画面中央,也不要把主题物品作为背景中心装饰。\n主题为:{}",
|
||||
clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT)
|
||||
)
|
||||
}
|
||||
|
||||
fn build_wooden_fish_back_button_prompt(prompt: &str) -> String {
|
||||
format!(
|
||||
"生成敲木鱼左上角返回按钮图。要求以参考图-去除绿色背景后的敲击物主体和背景环境图为主题、画风、材质和配色参考,但参考图只用来约束圆形底色和中央左箭头的颜色搭配,不要继承复杂造型、花纹、浮雕边、异形外框或装饰图案。按钮必须始终是标准圆形,整体像单个圆形图标,圆心居中,圆形内部只保留一个清晰、简洁、居中的向左返回箭头,不要出现文字、数字、水印、按钮外标签、额外 UI 面板、木槌或敲击道具。尺寸1:1,输出绿色背景主体图(纯绿色绿幕),背景必须是单一纯绿色 #00FF00 且平整无纹理、无渐变、无阴影。按钮主体边缘干净,后续由服务端扣除绿色背景;按钮底色不要使用与绿幕接近的纯绿色,若主题天然包含绿色,请仅在圆形底色上使用偏深、偏黄或偏蓝的主题绿色,并用更高对比的箭头颜色区分。\n主题为:{}",
|
||||
clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT)
|
||||
)
|
||||
}
|
||||
@@ -866,6 +830,39 @@ fn downloaded_wooden_fish_reference_image(
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_wooden_fish_hit_object_image_for_persist(
|
||||
image: DownloadedOpenAiImage,
|
||||
) -> Result<DownloadedOpenAiImage, AppError> {
|
||||
prepare_wooden_fish_green_screen_image_for_persist(image, "敲木鱼敲击物图案")
|
||||
}
|
||||
|
||||
fn prepare_wooden_fish_green_screen_image_for_persist(
|
||||
image: DownloadedOpenAiImage,
|
||||
failure_label: &str,
|
||||
) -> Result<DownloadedOpenAiImage, AppError> {
|
||||
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": WOODEN_FISH_CREATION_PROVIDER,
|
||||
"message": format!("{failure_label}解码失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
let mut encoded = std::io::Cursor::new(Vec::new());
|
||||
crate::generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha(source)
|
||||
.write_to(&mut encoded, image::ImageFormat::Png)
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": WOODEN_FISH_CREATION_PROVIDER,
|
||||
"message": format!("{failure_label}绿幕去背失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
|
||||
Ok(DownloadedOpenAiImage {
|
||||
bytes: encoded.into_inner(),
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
struct WoodenFishImageSlotPersistSpec {
|
||||
slot: &'static str,
|
||||
asset_kind: &'static str,
|
||||
@@ -1194,23 +1191,93 @@ mod tests {
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_hit_object_prompt_uses_hidden_image2_flow() {
|
||||
fn wooden_fish_hit_object_prompt_uses_hidden_green_screen_flow() {
|
||||
let prompt = build_wooden_fish_hit_object_prompt("赛博莲花木鱼");
|
||||
|
||||
assert_eq!(
|
||||
prompt,
|
||||
"生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。\n新主题为:赛博莲花木鱼"
|
||||
);
|
||||
assert!(prompt.contains(
|
||||
"生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。"
|
||||
));
|
||||
assert!(prompt.contains("尺寸1:1"));
|
||||
assert!(prompt.contains("绿色背景主体图"));
|
||||
assert!(prompt.contains("纯绿色绿幕"));
|
||||
assert!(prompt.contains("#00FF00"));
|
||||
assert!(prompt.contains("不要直接输出透明底"));
|
||||
assert!(prompt.contains("主体本身不要使用与绿幕接近的纯绿色"));
|
||||
assert!(prompt.contains("新主题为:赛博莲花木鱼"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_background_prompt_uses_hidden_image2_flow() {
|
||||
let prompt = build_wooden_fish_background_prompt("赛博莲花木鱼");
|
||||
let prompt = build_wooden_fish_background_prompt("苹果");
|
||||
|
||||
assert!(prompt.contains(
|
||||
"生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。"
|
||||
));
|
||||
assert!(prompt.contains("尺寸竖屏9:16"));
|
||||
assert!(prompt.contains("抠图完成后的透明图"));
|
||||
assert!(prompt.contains("不继承任何绿色底色"));
|
||||
assert!(prompt.contains("完整不透明的背景环境图"));
|
||||
assert!(prompt.contains("中央主体预留区"));
|
||||
assert!(prompt.contains("禁止出现主题主体"));
|
||||
assert!(prompt.contains("苹果"));
|
||||
assert!(prompt.contains("不得把主题物品画在画面中央"));
|
||||
assert!(prompt.contains("主题为:苹果"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_back_button_prompt_forces_plain_circular_icon() {
|
||||
let prompt = build_wooden_fish_back_button_prompt("玉米");
|
||||
|
||||
assert!(prompt.contains("参考图只用来约束圆形底色和中央左箭头的颜色搭配"));
|
||||
assert!(prompt.contains("按钮必须始终是标准圆形"));
|
||||
assert!(prompt.contains("圆形内部只保留一个清晰、简洁、居中的向左返回箭头"));
|
||||
assert!(prompt.contains("不要继承复杂造型、花纹、浮雕边、异形外框或装饰图案"));
|
||||
assert!(prompt.contains("不要出现文字、数字、水印、按钮外标签、额外 UI 面板、木槌或敲击道具"));
|
||||
assert!(prompt.contains("按钮底色不要使用与绿幕接近的纯绿色"));
|
||||
assert!(prompt.contains("主题为:玉米"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_hit_object_prepare_removes_green_screen_background() {
|
||||
let width = 12;
|
||||
let height = 12;
|
||||
let mut image = image::RgbaImage::from_pixel(width, height, image::Rgba([0, 255, 0, 255]));
|
||||
for y in 4..8 {
|
||||
for x in 4..8 {
|
||||
image.put_pixel(x, y, image::Rgba([190, 70, 42, 255]));
|
||||
}
|
||||
}
|
||||
image.put_pixel(6, 6, image::Rgba([18, 14, 12, 255]));
|
||||
let mut encoded = std::io::Cursor::new(Vec::new());
|
||||
image::DynamicImage::ImageRgba8(image)
|
||||
.write_to(&mut encoded, image::ImageFormat::Png)
|
||||
.expect("test image should encode");
|
||||
|
||||
let original_bytes = encoded.into_inner();
|
||||
let processed = prepare_wooden_fish_hit_object_image_for_persist(DownloadedOpenAiImage {
|
||||
bytes: original_bytes.clone(),
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
});
|
||||
let processed = processed.expect("processed image should succeed");
|
||||
let decoded = image::load_from_memory(processed.bytes.as_slice())
|
||||
.expect("processed image should decode")
|
||||
.to_rgba8();
|
||||
|
||||
assert_eq!(processed.mime_type, "image/png");
|
||||
assert_eq!(processed.extension, "png");
|
||||
assert_eq!(
|
||||
prompt,
|
||||
"生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。\n主题为:赛博莲花木鱼"
|
||||
decoded.get_pixel(0, 0).0[3],
|
||||
0,
|
||||
"绿幕背景必须在入库前去除"
|
||||
);
|
||||
assert_eq!(decoded.get_pixel(4, 4).0[3], 255);
|
||||
assert_eq!(
|
||||
decoded.get_pixel(6, 6).0[3],
|
||||
255,
|
||||
"敲击物内部深色细节不能被当成背景抠除"
|
||||
);
|
||||
assert_ne!(processed.bytes, original_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1273,37 +1340,41 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_audio_asset_maps_from_generated_sound_effect() {
|
||||
let asset = shared_contracts::creation_audio::CreationAudioAsset {
|
||||
task_id: "task-hit-sound-1".to_string(),
|
||||
provider: "vector-engine-vidu".to_string(),
|
||||
asset_object_id: Some("assetobj-hit-sound-1".to_string()),
|
||||
asset_kind: Some(WOODEN_FISH_HIT_SOUND_ASSET_KIND.to_string()),
|
||||
audio_src: "/generated-wooden-fish-assets/wooden-fish-profile-1/hit-sound.mp3"
|
||||
.to_string(),
|
||||
prompt: Some("清脆木鱼声".to_string()),
|
||||
title: None,
|
||||
updated_at: None,
|
||||
fn wooden_fish_default_hit_sound_asset_uses_bundled_mp3() {
|
||||
let asset = default_wooden_fish_hit_sound_asset();
|
||||
|
||||
assert_eq!(asset.asset_id, "wooden-fish-default-hit-sound");
|
||||
assert_eq!(asset.audio_src, "/wooden-fish/default-hit-sound.mp3");
|
||||
assert_eq!(
|
||||
asset.audio_object_key,
|
||||
"public/wooden-fish/default-hit-sound.mp3"
|
||||
);
|
||||
assert_eq!(asset.asset_object_id, "wooden-fish-default-hit-sound");
|
||||
assert_eq!(asset.source, "bundled-default");
|
||||
assert_eq!(asset.prompt.as_deref(), Some("默认木鱼音"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_draft_uses_default_hit_sound_asset_and_ignores_prompt() {
|
||||
let payload = WoodenFishWorkspaceCreateRequest {
|
||||
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
|
||||
work_title: "今日敲木鱼".to_string(),
|
||||
work_description: String::new(),
|
||||
theme_tags: vec!["敲木鱼".to_string()],
|
||||
hit_object_prompt: "金色木鱼".to_string(),
|
||||
hit_object_reference_image_src: None,
|
||||
hit_sound_prompt: Some("清脆木鱼声".to_string()),
|
||||
hit_sound_asset: None,
|
||||
floating_words: vec![],
|
||||
};
|
||||
|
||||
let mapped = map_generated_creation_audio_to_wooden_fish_asset(
|
||||
"wooden-fish-profile-1",
|
||||
"清脆木鱼声",
|
||||
asset,
|
||||
WOODEN_FISH_HIT_SOUND_DURATION_SECONDS,
|
||||
)
|
||||
.expect("generated sound effect should map to wooden fish audio asset");
|
||||
let draft = build_wooden_fish_draft(&payload);
|
||||
|
||||
assert_eq!(
|
||||
mapped.asset_id,
|
||||
"wooden-fish-profile-1-hit-sound-task-hit-sound-1"
|
||||
);
|
||||
assert_eq!(
|
||||
mapped.audio_object_key,
|
||||
"generated-wooden-fish-assets/wooden-fish-profile-1/hit-sound.mp3"
|
||||
);
|
||||
assert_eq!(mapped.asset_object_id, "assetobj-hit-sound-1");
|
||||
assert_eq!(mapped.source, "generated");
|
||||
assert_eq!(mapped.duration_ms, Some(3_000));
|
||||
assert!(draft.hit_sound_prompt.is_none());
|
||||
let asset = draft
|
||||
.hit_sound_asset
|
||||
.expect("default hit sound asset should be attached");
|
||||
assert_eq!(asset.audio_src, "/wooden-fish/default-hit-sound.mp3");
|
||||
assert_eq!(asset.prompt.as_deref(), Some("默认木鱼音"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user